From 0c2470183cf857d22f05333d9221cde8db3ff97c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 26 Jun 2024 11:19:21 -0700 Subject: [PATCH 001/222] release: Update version number to 12.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0c5d8fe9826..5ce76f3c3ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockly", - "version": "11.1.1", + "version": "12.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "11.1.1", + "version": "12.0.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 7826c53ef43..03443612832 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "11.1.1", + "version": "12.0.0", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly" From 989c91f6267a4a561aedb8ee532d4e3422db4dbf Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 27 Jun 2024 11:11:45 -0700 Subject: [PATCH 002/222] feat!: Add support for preserving block comment locations. (#8231) * feat: Add support for preserving block comment locations. * chore: format the tests. --- core/bubbles/textinput_bubble.ts | 32 ++++++++++++++- core/icons/comment_icon.ts | 67 ++++++++++++++++++++++++++++++- core/interfaces/i_comment_icon.ts | 7 ++++ core/xml.ts | 27 +++++++++++-- tests/mocha/block_test.js | 4 ++ tests/mocha/comment_test.js | 26 ++++++++++++ 6 files changed, 158 insertions(+), 5 deletions(-) diff --git a/core/bubbles/textinput_bubble.ts b/core/bubbles/textinput_bubble.ts index d7d1f5ae7db..d10619846a8 100644 --- a/core/bubbles/textinput_bubble.ts +++ b/core/bubbles/textinput_bubble.ts @@ -47,6 +47,9 @@ export class TextInputBubble extends Bubble { /** Functions listening for changes to the size of this bubble. */ private sizeChangeListeners: (() => void)[] = []; + /** Functions listening for changes to the location of this bubble. */ + private locationChangeListeners: (() => void)[] = []; + /** The text of this bubble. */ private text = ''; @@ -105,6 +108,11 @@ export class TextInputBubble extends Bubble { this.sizeChangeListeners.push(listener); } + /** Adds a change listener to be notified when this bubble's location changes. */ + addLocationChangeListener(listener: () => void) { + this.locationChangeListeners.push(listener); + } + /** Creates the editor UI for this bubble. */ private createEditor(container: SVGGElement): { inputRoot: SVGForeignObjectElement; @@ -212,10 +220,25 @@ export class TextInputBubble extends Bubble { /** @returns the size of this bubble. */ getSize(): Size { - // Overriden to be public. + // Overridden to be public. return super.getSize(); } + override moveDuringDrag(newLoc: Coordinate) { + super.moveDuringDrag(newLoc); + this.onLocationChange(); + } + + override setPositionRelativeToAnchor(left: number, top: number) { + super.setPositionRelativeToAnchor(left, top); + this.onLocationChange(); + } + + protected override positionByRect(rect = new Rect(0, 0, 0, 0)) { + super.positionByRect(rect); + this.onLocationChange(); + } + /** Handles mouse down events on the resize target. */ private onResizePointerDown(e: PointerEvent) { this.bringToFront(); @@ -297,6 +320,13 @@ export class TextInputBubble extends Bubble { listener(); } } + + /** Handles a location change event for the text area. Calls event listeners. */ + private onLocationChange() { + for (const listener of this.locationChangeListeners) { + listener(); + } + } } Css.register(` diff --git a/core/icons/comment_icon.ts b/core/icons/comment_icon.ts index df54560c557..c05748dcc5e 100644 --- a/core/icons/comment_icon.ts +++ b/core/icons/comment_icon.ts @@ -58,6 +58,9 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { /** The size of this comment (which is applied to the editable bubble). */ private bubbleSize = new Size(DEFAULT_BUBBLE_WIDTH, DEFAULT_BUBBLE_HEIGHT); + /** The location of the comment bubble in workspace coordinates. */ + private bubbleLocation?: Coordinate; + /** * The visibility of the bubble for this comment. * @@ -149,7 +152,13 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { } override onLocationChange(blockOrigin: Coordinate): void { + const oldLocation = this.workspaceLocation; super.onLocationChange(blockOrigin); + if (this.bubbleLocation) { + const newLocation = this.workspaceLocation; + const delta = Coordinate.difference(newLocation, oldLocation); + this.bubbleLocation = Coordinate.sum(this.bubbleLocation, delta); + } const anchorLocation = this.getAnchorLocation(); this.textInputBubble?.setAnchorLocation(anchorLocation); this.textBubble?.setAnchorLocation(anchorLocation); @@ -191,18 +200,43 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { return this.bubbleSize; } + /** + * Sets the location of the comment bubble in the workspace. + */ + setBubbleLocation(location: Coordinate) { + this.bubbleLocation = location; + this.textInputBubble?.moveDuringDrag(location); + this.textBubble?.moveDuringDrag(location); + } + + /** + * @returns the location of the comment bubble in the workspace. + */ + getBubbleLocation(): Coordinate | undefined { + return this.bubbleLocation; + } + /** * @returns the state of the comment as a JSON serializable value if the * comment has text. Otherwise returns null. */ saveState(): CommentState | null { if (this.text) { - return { + const state: CommentState = { 'text': this.text, 'pinned': this.bubbleIsVisible(), 'height': this.bubbleSize.height, 'width': this.bubbleSize.width, }; + const location = this.getBubbleLocation(); + if (location) { + state['x'] = this.sourceBlock.workspace.RTL + ? this.sourceBlock.workspace.getWidth() - + (location.x + this.bubbleSize.width) + : location.x; + state['y'] = location.y; + } + return state; } return null; } @@ -216,6 +250,16 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { ); this.bubbleVisiblity = state['pinned'] ?? false; this.setBubbleVisible(this.bubbleVisiblity); + let x = state['x']; + const y = state['y']; + renderManagement.finishQueuedRenders().then(() => { + if (x && y) { + x = this.sourceBlock.workspace.RTL + ? this.sourceBlock.workspace.getWidth() - (x + this.bubbleSize.width) + : x; + this.setBubbleLocation(new Coordinate(x, y)); + } + }); } override onClick(): void { @@ -259,6 +303,12 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { } } + onBubbleLocationChange(): void { + if (this.textInputBubble) { + this.bubbleLocation = this.textInputBubble.getRelativeToSurfaceXY(); + } + } + bubbleIsVisible(): boolean { return this.bubbleVisiblity; } @@ -308,8 +358,14 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { ); this.textInputBubble.setText(this.getText()); this.textInputBubble.setSize(this.bubbleSize, true); + if (this.bubbleLocation) { + this.textInputBubble.moveDuringDrag(this.bubbleLocation); + } this.textInputBubble.addTextChangeListener(() => this.onTextChange()); this.textInputBubble.addSizeChangeListener(() => this.onSizeChange()); + this.textInputBubble.addLocationChangeListener(() => + this.onBubbleLocationChange(), + ); } /** Shows the non editable text bubble for this comment. */ @@ -320,6 +376,9 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { this.getAnchorLocation(), this.getBubbleOwnerRect(), ); + if (this.bubbleLocation) { + this.textBubble.moveDuringDrag(this.bubbleLocation); + } } /** Hides any open bubbles owned by this comment. */ @@ -365,6 +424,12 @@ export interface CommentState { /** The width of the comment bubble. */ width?: number; + + /** The X coordinate of the comment bubble. */ + x?: number; + + /** The Y coordinate of the comment bubble. */ + y?: number; } registry.register(CommentIcon.TYPE, CommentIcon); diff --git a/core/interfaces/i_comment_icon.ts b/core/interfaces/i_comment_icon.ts index 09b071110dd..2762348dea2 100644 --- a/core/interfaces/i_comment_icon.ts +++ b/core/interfaces/i_comment_icon.ts @@ -8,6 +8,7 @@ import {IconType} from '../icons/icon_types.js'; import {CommentState} from '../icons/comment_icon.js'; import {IIcon, isIcon} from './i_icon.js'; import {Size} from '../utils/size.js'; +import {Coordinate} from '../utils/coordinate.js'; import {IHasBubble, hasBubble} from './i_has_bubble.js'; import {ISerializable, isSerializable} from './i_serializable.js'; @@ -20,6 +21,10 @@ export interface ICommentIcon extends IIcon, IHasBubble, ISerializable { getBubbleSize(): Size; + setBubbleLocation(location: Coordinate): void; + + getBubbleLocation(): Coordinate | undefined; + saveState(): CommentState; loadState(state: CommentState): void; @@ -35,6 +40,8 @@ export function isCommentIcon(obj: Object): obj is ICommentIcon { (obj as any)['getText'] !== undefined && (obj as any)['setBubbleSize'] !== undefined && (obj as any)['getBubbleSize'] !== undefined && + (obj as any)['setBubbleLocation'] !== undefined && + (obj as any)['getBubbleLocation'] !== undefined && obj.getType() === IconType.COMMENT ); } diff --git a/core/xml.ts b/core/xml.ts index bad381c5d85..b8ecf6433d8 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -217,12 +217,24 @@ export function blockToDom( const comment = block.getIcon(IconType.COMMENT)!; const size = comment.getBubbleSize(); const pinned = comment.bubbleIsVisible(); + const location = comment.getBubbleLocation(); const commentElement = utilsXml.createElement('comment'); commentElement.appendChild(utilsXml.createTextNode(commentText)); commentElement.setAttribute('pinned', `${pinned}`); - commentElement.setAttribute('h', String(size.height)); - commentElement.setAttribute('w', String(size.width)); + commentElement.setAttribute('h', `${size.height}`); + commentElement.setAttribute('w', `${size.width}`); + if (location) { + commentElement.setAttribute( + 'x', + `${ + block.workspace.RTL + ? block.workspace.getWidth() - (location.x + size.width) + : location.x + }`, + ); + commentElement.setAttribute('y', `${location.y}`); + } element.appendChild(commentElement); } @@ -795,6 +807,8 @@ function applyCommentTagNodes(xmlChildren: Element[], block: Block) { const pinned = xmlChild.getAttribute('pinned') === 'true'; const width = parseInt(xmlChild.getAttribute('w') ?? '50', 10); const height = parseInt(xmlChild.getAttribute('h') ?? '50', 10); + let x = parseInt(xmlChild.getAttribute('x') ?? '', 10); + const y = parseInt(xmlChild.getAttribute('y') ?? '', 10); block.setCommentText(text); const comment = block.getIcon(IconType.COMMENT)!; @@ -803,8 +817,15 @@ function applyCommentTagNodes(xmlChildren: Element[], block: Block) { } // Set the pinned state of the bubble. comment.setBubbleVisible(pinned); + // Actually show the bubble after the block has been rendered. - setTimeout(() => comment.setBubbleVisible(pinned), 1); + setTimeout(() => { + if (!isNaN(x) && !isNaN(y)) { + x = block.workspace.RTL ? block.workspace.getWidth() - (x + width) : x; + comment.setBubbleLocation(new Coordinate(x, y)); + } + comment.setBubbleVisible(pinned); + }, 1); } } diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index dd070f86cbb..d7c0d7c58a3 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -1388,6 +1388,10 @@ suite('Blocks', function () { return Blockly.utils.Size(0, 0); } + setBubbleLocation() {} + + getBubbleLocation() {} + bubbleIsVisible() { return true; } diff --git a/tests/mocha/comment_test.js b/tests/mocha/comment_test.js index d4091b9c2d3..1f392194fd2 100644 --- a/tests/mocha/comment_test.js +++ b/tests/mocha/comment_test.js @@ -141,4 +141,30 @@ suite('Comments', function () { assertBubbleSize(this.comment, 100, 100); }); }); + suite('Set/Get Bubble Location', function () { + teardown(function () { + sinon.restore(); + }); + function assertBubbleLocation(comment, x, y) { + const location = comment.getBubbleLocation(); + assert.equal(location.x, x); + assert.equal(location.y, y); + } + test('Set Location While Visible', function () { + this.comment.setBubbleVisible(true); + + this.comment.setBubbleLocation(new Blockly.utils.Coordinate(100, 100)); + assertBubbleLocation(this.comment, 100, 100); + + this.comment.setBubbleVisible(false); + assertBubbleLocation(this.comment, 100, 100); + }); + test('Set Location While Invisible', function () { + this.comment.setBubbleLocation(new Blockly.utils.Coordinate(100, 100)); + assertBubbleLocation(this.comment, 100, 100); + + this.comment.setBubbleVisible(true); + assertBubbleLocation(this.comment, 100, 100); + }); + }); }); From f45270e0837b4da5c899943fae62bcefed28d836 Mon Sep 17 00:00:00 2001 From: Shreyans Pathak Date: Fri, 12 Jul 2024 12:11:19 -0400 Subject: [PATCH 003/222] refactor: field_checkbox `dom.addClass` params (#8309) --- core/field_checkbox.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/field_checkbox.ts b/core/field_checkbox.ts index 83f460bb9d2..0773a1f8251 100644 --- a/core/field_checkbox.ts +++ b/core/field_checkbox.ts @@ -114,7 +114,7 @@ export class FieldCheckbox extends Field { super.initView(); const textElement = this.getTextElement(); - dom.addClass(textElement, 'blocklyCheckbox'); + dom.addClass(this.fieldGroup_!, 'blocklyCheckboxField'); textElement.style.display = this.value_ ? 'block' : 'none'; } From 9ba791c1446b088e423bfbbb55517ee8fbda5675 Mon Sep 17 00:00:00 2001 From: Gabriel Fleury <55366345+ga-fleury@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:34:42 -0300 Subject: [PATCH 004/222] bug: Rename the blockly icon CSS classes to use camelCase (#8329) (#8335) --- core/icons/comment_icon.ts | 2 +- core/icons/mutator_icon.ts | 2 +- core/icons/warning_icon.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/icons/comment_icon.ts b/core/icons/comment_icon.ts index c05748dcc5e..064f6784347 100644 --- a/core/icons/comment_icon.ts +++ b/core/icons/comment_icon.ts @@ -114,7 +114,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { }, this.svgRoot, ); - dom.addClass(this.svgRoot!, 'blockly-icon-comment'); + dom.addClass(this.svgRoot!, 'blocklyCommentIcon'); } override dispose() { diff --git a/core/icons/mutator_icon.ts b/core/icons/mutator_icon.ts index 7fb3fcf3b81..2c350f544ea 100644 --- a/core/icons/mutator_icon.ts +++ b/core/icons/mutator_icon.ts @@ -116,7 +116,7 @@ export class MutatorIcon extends Icon implements IHasBubble { {'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'}, this.svgRoot, ); - dom.addClass(this.svgRoot!, 'blockly-icon-mutator'); + dom.addClass(this.svgRoot!, 'blocklyMutatorIcon'); } override dispose(): void { diff --git a/core/icons/warning_icon.ts b/core/icons/warning_icon.ts index 08f511a60b7..5a22ec16d25 100644 --- a/core/icons/warning_icon.ts +++ b/core/icons/warning_icon.ts @@ -89,7 +89,7 @@ export class WarningIcon extends Icon implements IHasBubble { }, this.svgRoot, ); - dom.addClass(this.svgRoot!, 'blockly-icon-warning'); + dom.addClass(this.svgRoot!, 'blocklyWarningIcon'); } override dispose() { From 5a32c3fe43c2f14d8cf6c1f88eab6f9569d1cd46 Mon Sep 17 00:00:00 2001 From: Suryansh Shakya <83297944+nullHawk@users.noreply.github.com> Date: Fri, 12 Jul 2024 22:05:10 +0530 Subject: [PATCH 005/222] feat: added blocklyField to field's SVG Group (#8334) --- core/field.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/field.ts b/core/field.ts index 51e006823d7..cb0fadb03e6 100644 --- a/core/field.ts +++ b/core/field.ts @@ -323,6 +323,9 @@ export abstract class Field protected initView() { this.createBorderRect_(); this.createTextElement_(); + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyField'); + } } /** From dd18edd3439fa24bd37b92131c6e0c2997c848a3 Mon Sep 17 00:00:00 2001 From: Shashwat Pathak <111122076+shashwatpathak98@users.noreply.github.com> Date: Fri, 12 Jul 2024 22:06:25 +0530 Subject: [PATCH 006/222] fix!: Make `IPathObject` styling methods optional (#8332) --- core/block_svg.ts | 6 +++--- core/renderers/common/i_path_object.ts | 30 +++++++++++++------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index bc02ef63810..ac6970dced8 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -849,7 +849,7 @@ export class BlockSvg * @internal */ applyColour() { - this.pathObject.applyColour(this); + this.pathObject.applyColour?.(this); const icons = this.getIcons(); for (let i = 0; i < icons.length; i++) { @@ -1115,7 +1115,7 @@ export class BlockSvg .getConstants() .getBlockStyleForColour(this.colour_); - this.pathObject.setStyle(styleObj.style); + this.pathObject.setStyle?.(styleObj.style); this.style = styleObj.style; this.styleName_ = styleObj.name; @@ -1137,7 +1137,7 @@ export class BlockSvg if (blockStyle) { this.hat = blockStyle.hat; - this.pathObject.setStyle(blockStyle); + this.pathObject.setStyle?.(blockStyle); // Set colour to match Block. this.colour_ = blockStyle.colourPrimary; this.style = blockStyle; diff --git a/core/renderers/common/i_path_object.ts b/core/renderers/common/i_path_object.ts index 30033f18e81..cb647520257 100644 --- a/core/renderers/common/i_path_object.ts +++ b/core/renderers/common/i_path_object.ts @@ -49,21 +49,6 @@ export interface IPathObject { */ setPath(pathString: string): void; - /** - * Apply the stored colours to the block's path, taking into account whether - * the paths belong to a shadow block. - * - * @param block The source block. - */ - applyColour(block: BlockSvg): void; - - /** - * Update the style. - * - * @param blockStyle The block style to use. - */ - setStyle(blockStyle: BlockStyle): void; - /** * Flip the SVG paths in RTL. */ @@ -130,8 +115,23 @@ export interface IPathObject { rtl: boolean, ): void; + /** + * Apply the stored colours to the block's path, taking into account whether + * the paths belong to a shadow block. + * + * @param block The source block. + */ + applyColour?(block: BlockSvg): void; + /** * Removes any highlight associated with the given connection, if it exists. */ removeConnectionHighlight?(connection: RenderedConnection): void; + + /** + * Update the style. + * + * @param blockStyle The block style to use. + */ + setStyle?(blockStyle: BlockStyle): void; } From 968494205a03bb92cfc514b97d0c78172abcc36c Mon Sep 17 00:00:00 2001 From: Gabriel Fleury <55366345+ga-fleury@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:37:20 -0300 Subject: [PATCH 007/222] feat: Add a blocklyFieldText CSS class to fields' text elements (#8291) (#8302) * feat!: Add a blocklyFieldText CSS class to fields' text elements (#8291) * add class instead of replace Co-authored-by: Beka Westberg --------- Co-authored-by: Beka Westberg --- core/field.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/field.ts b/core/field.ts index cb0fadb03e6..eed34e613fc 100644 --- a/core/field.ts +++ b/core/field.ts @@ -376,7 +376,7 @@ export abstract class Field this.textElement_ = dom.createSvgElement( Svg.TEXT, { - 'class': 'blocklyText', + 'class': 'blocklyText blocklyFieldText', }, this.fieldGroup_, ); From 7c22c46ee685815fbbdd06764bee3f929736c873 Mon Sep 17 00:00:00 2001 From: Shreyans Pathak Date: Mon, 15 Jul 2024 14:54:30 -0400 Subject: [PATCH 008/222] refactor: Add `addClass` and `removeClass` methods to blockSvg (#8337) * refactor: Add `addClass` and `removeClass` methods to blockSvg * fix: lint * fix: jsdoc --- core/block_svg.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index ac6970dced8..adef213d5fc 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -671,6 +671,24 @@ export class BlockSvg } } + /** + * Add a CSS class to the SVG group of this block. + * + * @param className + */ + addClass(className: string) { + dom.addClass(this.svgGroup_, className); + } + + /** + * Remove a CSS class from the SVG group of this block. + * + * @param className + */ + removeClass(className: string) { + dom.removeClass(this.svgGroup_, className); + } + /** * Recursively adds or removes the dragging class to this node and its * children. @@ -683,10 +701,10 @@ export class BlockSvg if (adding) { this.translation = ''; common.draggingConnections.push(...this.getConnections_(true)); - dom.addClass(this.svgGroup_, 'blocklyDragging'); + this.addClass('blocklyDragging'); } else { common.draggingConnections.length = 0; - dom.removeClass(this.svgGroup_, 'blocklyDragging'); + this.removeClass('blocklyDragging'); } // Recurse through all blocks attached under this one. for (let i = 0; i < this.childBlocks_.length; i++) { From 00d090edcf3c5ab83dc27b93c061dd1617dc6616 Mon Sep 17 00:00:00 2001 From: Shashwat Pathak <111122076+shashwatpathak98@users.noreply.github.com> Date: Tue, 16 Jul 2024 01:58:39 +0530 Subject: [PATCH 009/222] feat: Add a `blocklyVariableField` CSS class to variable fields (#8359) --- core/field_variable.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/field_variable.ts b/core/field_variable.ts index 539557256b6..d0a929bf014 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -22,6 +22,7 @@ import { MenuGenerator, MenuOption, } from './field_dropdown.js'; +import * as dom from './utils/dom.js'; import * as fieldRegistry from './field_registry.js'; import * as internalConstants from './internal_constants.js'; import type {Menu} from './menu.js'; @@ -148,6 +149,11 @@ export class FieldVariable extends FieldDropdown { this.doValueUpdate_(variable.getId()); } + override initView() { + super.initView(); + dom.addClass(this.fieldGroup_!, 'blocklyVariableField'); + } + override shouldAddBorderRect_() { const block = this.getSourceBlock(); if (!block) { From aecfe34c381ef17c721f1d533f60e4c0aa24032a Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 15 Jul 2024 15:29:19 -0700 Subject: [PATCH 010/222] feat: add the IVariableMap and IVariableModel interfaces. (#8369) * feat: add the IVariableMap and IVariableModel interfaces. * chore: add license headers. --- core/blockly.ts | 4 ++ core/interfaces/i_variable_map.ts | 72 +++++++++++++++++++++++++++++ core/interfaces/i_variable_model.ts | 26 +++++++++++ 3 files changed, 102 insertions(+) create mode 100644 core/interfaces/i_variable_map.ts create mode 100644 core/interfaces/i_variable_model.ts diff --git a/core/blockly.ts b/core/blockly.ts index 77362c0b4b1..28eb0010a6b 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -149,6 +149,8 @@ import {ISerializable, isSerializable} from './interfaces/i_serializable.js'; import {IStyleable} from './interfaces/i_styleable.js'; import {IToolbox} from './interfaces/i_toolbox.js'; import {IToolboxItem} from './interfaces/i_toolbox_item.js'; +import {IVariableMap} from './interfaces/i_variable_map.js'; +import {IVariableModel} from './interfaces/i_variable_model.js'; import { IVariableBackedParameterModel, isVariableBackedParameterModel, @@ -552,6 +554,8 @@ export {ISerializable, isSerializable}; export {IStyleable}; export {IToolbox}; export {IToolboxItem}; +export {IVariableMap}; +export {IVariableModel}; export {IVariableBackedParameterModel, isVariableBackedParameterModel}; export {Marker}; export {MarkerManager}; diff --git a/core/interfaces/i_variable_map.ts b/core/interfaces/i_variable_map.ts new file mode 100644 index 00000000000..0bfc532a76e --- /dev/null +++ b/core/interfaces/i_variable_map.ts @@ -0,0 +1,72 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {IVariableModel} from './i_variable_model.js'; +import {State} from '../serialization/variables.js'; + +/** + * Variable maps are container objects responsible for storing and managing the + * set of variables referenced on a workspace. + * + * Any of these methods may define invariants about which names and types are + * legal, and throw if they are not met. + */ +export interface IVariableMap { + /* Returns the variable corresponding to the given ID, or null if none. */ + getVariableById(id: string): T | null; + + /** + * Returns the variable with the given name, or null if not found. If `type` + * is provided, the variable's type must also match, or null should be + * returned. + */ + getVariable(name: string, type?: string): T | null; + + /* Returns a list of all variables managed by this variable map. */ + getAllVariables(): T[]; + + /** + * Returns a list of all of the variables of the given type managed by this + * variable map. + */ + getVariablesOfType(type: string): T[]; + + /** + * Returns a list of the set of types of the variables managed by this + * variable map. + */ + getTypes(): string[]; + + /** + * Creates a new variable with the given name. If ID is not specified, the + * variable map should create one. Returns the new variable. + */ + createVariable(name: string, id?: string, type?: string | null): T; + + /** + * Changes the name of the given variable to the name provided and returns the + * renamed variable. + */ + renameVariable(variable: T, newName: string): T; + + /* Changes the type of the given variable and returns it. */ + changeVariableType(variable: T, newType: string): T; + + /* Deletes the given variable. */ + deleteVariable(variable: T): void; + + /* Removes all variables from this variable map. */ + clear(): void; + + /* Returns an object representing the serialized state of the variable. */ + saveVariable(variable: T): U; + + /** + * Creates a variable in this variable map corresponding to the given state + * (produced by a call to `saveVariable`). + */ + loadVariable(state: U): T; +} diff --git a/core/interfaces/i_variable_model.ts b/core/interfaces/i_variable_model.ts new file mode 100644 index 00000000000..97fa9161d41 --- /dev/null +++ b/core/interfaces/i_variable_model.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/* Representation of a variable. */ +export interface IVariableModel { + /* Returns the unique ID of this variable. */ + getId(): string; + + /* Returns the user-visible name of this variable. */ + getName(): string; + + /** + * Returns the type of the variable like 'int' or 'string'. Does not need to be + * unique. This will default to '' which is a specific type. + */ + getType(): string; + + /* Sets the user-visible name of this variable. */ + setName(name: string): this; + + /* Sets the type of this variable. */ + setType(type: string): this; +} From ae2fea484f63b97c6885a211c9920a5d75d921fa Mon Sep 17 00:00:00 2001 From: Abhinav Choudhary Date: Tue, 16 Jul 2024 23:30:32 +0530 Subject: [PATCH 011/222] fix!: Rename `blocklyTreeRow` and `blocklyToolboxCategory` CSS classes (#8357) * fix!: #8345 rename css class This commit renames the blocklyTreeRow CSS class to blocklyToolboxCategory * Update category.ts * fix: css class conflicts Rename original blocklyToolboxCategory to blocklyToolboxCategoryContainer --- core/toolbox/category.ts | 14 +++++++------- tests/mocha/comment_deserialization_test.js | 2 +- tests/mocha/toolbox_test.js | 4 +++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/core/toolbox/category.ts b/core/toolbox/category.ts index 6ff159b0b02..2d0ea4aee24 100644 --- a/core/toolbox/category.ts +++ b/core/toolbox/category.ts @@ -131,8 +131,8 @@ export class ToolboxCategory */ protected makeDefaultCssConfig_(): CssConfig { return { - 'container': 'blocklyToolboxCategory', - 'row': 'blocklyTreeRow', + 'container': 'blocklyToolboxCategoryContainer', + 'row': 'blocklyToolboxCategory', 'rowcontentcontainer': 'blocklyTreeRowContentContainer', 'icon': 'blocklyTreeIcon', 'label': 'blocklyTreeLabel', @@ -659,19 +659,19 @@ export type CssConfig = ToolboxCategory.CssConfig; /** CSS for Toolbox. See css.js for use. */ Css.register(` -.blocklyTreeRow:not(.blocklyTreeSelected):hover { +.blocklyToolboxCategory:not(.blocklyTreeSelected):hover { background-color: rgba(255, 255, 255, .2); } -.blocklyToolboxDiv[layout="h"] .blocklyToolboxCategory { +.blocklyToolboxDiv[layout="h"] .blocklyToolboxCategoryContainer { margin: 1px 5px 1px 0; } -.blocklyToolboxDiv[dir="RTL"][layout="h"] .blocklyToolboxCategory { +.blocklyToolboxDiv[dir="RTL"][layout="h"] .blocklyToolboxCategoryContainer { margin: 1px 0 1px 5px; } -.blocklyTreeRow { +.blocklyToolboxCategory { height: 22px; line-height: 22px; margin-bottom: 3px; @@ -679,7 +679,7 @@ Css.register(` white-space: nowrap; } -.blocklyToolboxDiv[dir="RTL"] .blocklyTreeRow { +.blocklyToolboxDiv[dir="RTL"] .blocklyToolboxCategory { margin-left: 8px; padding-right: 0; } diff --git a/tests/mocha/comment_deserialization_test.js b/tests/mocha/comment_deserialization_test.js index 2517ed77982..54ee0b2ff30 100644 --- a/tests/mocha/comment_deserialization_test.js +++ b/tests/mocha/comment_deserialization_test.js @@ -110,7 +110,7 @@ suite('Comment Deserialization', function () { test('Toolbox', function () { // Place from toolbox. const toolbox = this.workspace.getToolbox(); - simulateClick(toolbox.HtmlDiv.querySelector('.blocklyTreeRow')); + simulateClick(toolbox.HtmlDiv.querySelector('.blocklyToolboxCategory')); simulateClick( toolbox.getFlyout().svgGroup_.querySelector('.blocklyPath'), ); diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js index b723e703803..b3cd45090dc 100644 --- a/tests/mocha/toolbox_test.js +++ b/tests/mocha/toolbox_test.js @@ -201,7 +201,9 @@ suite('Toolbox', function () { sinon.assert.calledOnce(hideChaffStub); }); test('Category clicked -> Should select category', function () { - const categoryXml = document.getElementsByClassName('blocklyTreeRow')[0]; + const categoryXml = document.getElementsByClassName( + 'blocklyToolboxCategory', + )[0]; const evt = { 'target': categoryXml, }; From c5532066f5ccec34af079d898d60d36d8a4597d0 Mon Sep 17 00:00:00 2001 From: Nirmal Kumar Date: Wed, 17 Jul 2024 01:39:49 +0530 Subject: [PATCH 012/222] feat: Add a blocklyTextBubble CSS class to the text bubble #8331 (#8333) --- core/bubbles/text_bubble.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/bubbles/text_bubble.ts b/core/bubbles/text_bubble.ts index 020ab4f2ec1..492bced133f 100644 --- a/core/bubbles/text_bubble.ts +++ b/core/bubbles/text_bubble.ts @@ -27,6 +27,7 @@ export class TextBubble extends Bubble { super(workspace, anchor, ownerRect); this.paragraph = this.stringToSvg(text, this.contentContainer); this.updateBubbleSize(); + dom.addClass(this.svgRoot, 'blocklyTextBubble'); } /** @returns the current text of this text bubble. */ From bef8d8319d3cfb2ce9514ec84fae5a81a3f17389 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 16 Jul 2024 15:47:43 -0700 Subject: [PATCH 013/222] refactor: make VariableModel implement IVariableModel. (#8381) * refactor: make VariableModel implement IVariableModel. * chore: assauge the linter. --- core/variable_model.ts | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/core/variable_model.ts b/core/variable_model.ts index 0728a3d564d..c4ec05367e2 100644 --- a/core/variable_model.ts +++ b/core/variable_model.ts @@ -16,6 +16,7 @@ import './events/events_var_create.js'; import * as idGenerator from './utils/idgenerator.js'; import type {Workspace} from './workspace.js'; +import {IVariableModel} from './interfaces/i_variable_model.js'; /** * Class for a variable model. @@ -23,7 +24,7 @@ import type {Workspace} from './workspace.js'; * * @see {Blockly.FieldVariable} */ -export class VariableModel { +export class VariableModel implements IVariableModel { type: string; private readonly id_: string; @@ -64,6 +65,36 @@ export class VariableModel { return this.id_; } + /** @returns The name of this variable. */ + getName(): string { + return this.name; + } + + /** + * Updates the user-visible name of this variable. + * + * @returns The newly-updated variable. + */ + setName(newName: string): this { + this.name = newName; + return this; + } + + /** @returns The type of this variable. */ + getType(): string { + return this.type; + } + + /** + * Updates the type of this variable. + * + * @returns The newly-updated variable. + */ + setType(newType: string): this { + this.type = newType; + return this; + } + /** * A custom compare function for the VariableModel objects. * From 8cca066bcf7130f8c57890ec508decf7bbd0307b Mon Sep 17 00:00:00 2001 From: Devesh Rahatekar <79015420+devesh-2002@users.noreply.github.com> Date: Wed, 17 Jul 2024 22:14:24 +0530 Subject: [PATCH 014/222] feat: Add a blocklyShadow class (#8336) * feat: Add blockShadow class * formatted the file --- core/renderers/common/path_object.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index d5c0850a1b8..8ca8cd19324 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -186,8 +186,11 @@ export class PathObject implements IPathObject { */ protected updateShadow_(shadow: boolean) { if (shadow) { + this.setClass_('blocklyShadow', true); this.svgPath.setAttribute('stroke', 'none'); this.svgPath.setAttribute('fill', this.style.colourSecondary); + } else { + this.setClass_('blocklyShadow', false); } } From 33b53718eb7193771327bd6e4b70b05381c0e946 Mon Sep 17 00:00:00 2001 From: Krishnakumar Chavan <58606754+krishchvn@users.noreply.github.com> Date: Wed, 17 Jul 2024 12:45:36 -0400 Subject: [PATCH 015/222] fix!: renamed blocklyTreeIcon Css class to blocklyToolboxCategoryIcon #8347 (#8367) * renamed blocklyTreeIcon Css class to blocklyToolboxCategoryIcon * fix!: renamed blocklyTreeIcon Css class to blocklyToolboxCategoryIcon #8347 * fixed whitespace formatting --- core/toolbox/category.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/toolbox/category.ts b/core/toolbox/category.ts index 2d0ea4aee24..bbcb6bf6ff1 100644 --- a/core/toolbox/category.ts +++ b/core/toolbox/category.ts @@ -134,7 +134,7 @@ export class ToolboxCategory 'container': 'blocklyToolboxCategoryContainer', 'row': 'blocklyToolboxCategory', 'rowcontentcontainer': 'blocklyTreeRowContentContainer', - 'icon': 'blocklyTreeIcon', + 'icon': 'blocklyToolboxCategoryIcon', 'label': 'blocklyTreeLabel', 'contents': 'blocklyToolboxContents', 'selected': 'blocklyTreeSelected', @@ -684,7 +684,7 @@ Css.register(` padding-right: 0; } -.blocklyTreeIcon { +.blocklyToolboxCategoryIcon { background-image: url(<<>>/sprites.png); height: 16px; vertical-align: middle; From ae80adfe9cb51890241cff435c30416804da401a Mon Sep 17 00:00:00 2001 From: Arun Chandran <53257113+Arun-cn@users.noreply.github.com> Date: Wed, 17 Jul 2024 22:48:14 +0530 Subject: [PATCH 016/222] fix!: Replace Closure UI CSS classes with Blockly CSS classes (#8339) * fix!: Replace Closure UI CSS classes with Blockly CSS classes * chore: remove comments about deprecated goog-x class * chore: remove deprecated goog-x classes * fix: correct coding format to pass CI checks --- core/menu.ts | 3 +-- core/menuitem.ts | 23 +++++++---------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/core/menu.ts b/core/menu.ts index b0fb5557346..29615925bc9 100644 --- a/core/menu.ts +++ b/core/menu.ts @@ -85,8 +85,7 @@ export class Menu { */ render(container: Element): HTMLDivElement { const element = document.createElement('div'); - // goog-menu is deprecated, use blocklyMenu. May 2020. - element.className = 'blocklyMenu goog-menu blocklyNonSelectable'; + element.className = 'blocklyMenu blocklyNonSelectable'; element.tabIndex = 0; if (this.roleName) { aria.setRole(element, this.roleName); diff --git a/core/menuitem.ts b/core/menuitem.ts index aad914b6889..7fff1a72bbc 100644 --- a/core/menuitem.ts +++ b/core/menuitem.ts @@ -64,22 +64,19 @@ export class MenuItem { this.element = element; // Set class and style - // goog-menuitem* is deprecated, use blocklyMenuItem*. May 2020. element.className = - 'blocklyMenuItem goog-menuitem ' + - (this.enabled ? '' : 'blocklyMenuItemDisabled goog-menuitem-disabled ') + - (this.checked ? 'blocklyMenuItemSelected goog-option-selected ' : '') + - (this.highlight - ? 'blocklyMenuItemHighlight goog-menuitem-highlight ' - : '') + - (this.rightToLeft ? 'blocklyMenuItemRtl goog-menuitem-rtl ' : ''); + 'blocklyMenuItem ' + + (this.enabled ? '' : 'blocklyMenuItemDisabled ') + + (this.checked ? 'blocklyMenuItemSelected ' : '') + + (this.highlight ? 'blocklyMenuItemHighlight ' : '') + + (this.rightToLeft ? 'blocklyMenuItemRtl ' : ''); const content = document.createElement('div'); - content.className = 'blocklyMenuItemContent goog-menuitem-content'; + content.className = 'blocklyMenuItemContent'; // Add a checkbox for checkable menu items. if (this.checkable) { const checkbox = document.createElement('div'); - checkbox.className = 'blocklyMenuItemCheckbox goog-menuitem-checkbox'; + checkbox.className = 'blocklyMenuItemCheckbox '; content.appendChild(checkbox); } @@ -188,19 +185,13 @@ export class MenuItem { */ setHighlighted(highlight: boolean) { this.highlight = highlight; - const el = this.getElement(); if (el && this.isEnabled()) { - // goog-menuitem-highlight is deprecated, use blocklyMenuItemHighlight. - // May 2020. const name = 'blocklyMenuItemHighlight'; - const nameDep = 'goog-menuitem-highlight'; if (highlight) { dom.addClass(el, name); - dom.addClass(el, nameDep); } else { dom.removeClass(el, name); - dom.removeClass(el, nameDep); } } } From e298f5541256d80c43989e2ee12efddb6ddf7c70 Mon Sep 17 00:00:00 2001 From: Chaitanya Yeole <77329060+ChaitanyaYeole02@users.noreply.github.com> Date: Wed, 17 Jul 2024 13:19:53 -0400 Subject: [PATCH 017/222] feat: Added blocklyTrashcanFlyout CSS class (#8372) * feat: Add blocklyTrashcanFlyout class * Fixed formatting issues * fix: versioning reverted to original * fix: prettier version resolved * fix: clean installation --- core/trashcan.ts | 7 +- package-lock.json | 2783 +++++++++++++++++++++------------------------ 2 files changed, 1327 insertions(+), 1463 deletions(-) diff --git a/core/trashcan.ts b/core/trashcan.ts index 050f506a48f..7caee837ea5 100644 --- a/core/trashcan.ts +++ b/core/trashcan.ts @@ -239,10 +239,9 @@ export class Trashcan /** Initializes the trash can. */ init() { if (this.workspace.options.maxTrashcanContents > 0) { - dom.insertAfter( - this.flyout!.createDom(Svg.SVG)!, - this.workspace.getParentSvg(), - ); + const flyoutDom = this.flyout!.createDom(Svg.SVG)!; + dom.addClass(flyoutDom, 'blocklyTrashcanFlyout'); + dom.insertAfter(flyoutDom, this.workspace.getParentSvg()); this.flyout!.init(this.workspace); } this.workspace.getComponentManager().addComponent({ diff --git a/package-lock.json b/package-lock.json index a5249e6b187..c809034ebaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -397,31 +397,6 @@ "node": ">=0.10.0" } }, - "node_modules/@gulp-sourcemaps/map-sources/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -531,18 +506,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", @@ -836,64 +799,24 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.4.6.tgz", - "integrity": "sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", + "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", "dev": true, "dependencies": { "debug": "4.3.4", "extract-zip": "2.0.1", "progress": "2.0.3", - "proxy-agent": "6.3.0", + "proxy-agent": "6.3.1", "tar-fs": "3.0.4", "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" + "yargs": "17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" }, "engines": { "node": ">=16.3.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@puppeteer/browsers/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@puppeteer/browsers/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" } }, "node_modules/@rushstack/node-core-library": { @@ -1173,18 +1096,18 @@ "dev": true }, "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.11", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", + "integrity": "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, "optional": true, "dependencies": { @@ -1682,18 +1605,6 @@ "node": "^16.13 || >=18" } }, - "node_modules/@wdio/config/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@wdio/logger": { "version": "8.38.0", "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", @@ -1709,18 +1620,6 @@ "node": "^16.13 || >=18" } }, - "node_modules/@wdio/logger/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/@wdio/logger/node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -1802,67 +1701,6 @@ "node": "^16.13 || >=18" } }, - "node_modules/@wdio/utils/node_modules/@puppeteer/browsers": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", - "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", - "dev": true, - "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "progress": "2.0.3", - "proxy-agent": "6.3.1", - "tar-fs": "3.0.4", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=16.3.0" - } - }, - "node_modules/@wdio/utils/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@wdio/utils/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/@wdio/utils/node_modules/proxy-agent": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", - "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", - "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.1", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -1922,9 +1760,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dependencies": { "debug": "^4.3.4" }, @@ -2009,12 +1847,15 @@ } }, "node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { @@ -2048,9 +1889,9 @@ "dev": true }, "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "dependencies": { "normalize-path": "^3.0.0", @@ -2063,7 +1904,7 @@ "node_modules/append-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", - "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", + "integrity": "sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA==", "dev": true, "dependencies": { "buffer-equal": "^1.0.0" @@ -2201,15 +2042,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/archiver/node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/archiver/node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -2258,7 +2090,7 @@ "node_modules/archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, "node_modules/are-docs-informative": { @@ -2277,12 +2109,12 @@ "dev": true }, "node_modules/aria-query": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", - "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, - "engines": { - "node": ">=6.0" + "dependencies": { + "dequal": "^2.0.3" } }, "node_modules/arr-diff": { @@ -2297,7 +2129,7 @@ "node_modules/arr-filter": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", - "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", + "integrity": "sha512-A2BETWCqhsecSvCkWAeVBFLH6sXEUGASuzkpjL3GR1SlL/PWL6M3J8EAAld2Uubmh39tvkJTqC9LeLHCUKmFXA==", "dev": true, "dependencies": { "make-iterator": "^1.0.0" @@ -2318,7 +2150,7 @@ "node_modules/arr-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", - "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", + "integrity": "sha512-tVqVTHt+Q5Xb09qRkbu+DidW1yYzz5izWS2Xm2yFm7qJnmUfz4HPzNxbHkdRJbz2lrqI7S+z17xNYdFcBBO8Hw==", "dev": true, "dependencies": { "make-iterator": "^1.0.0" @@ -2339,7 +2171,7 @@ "node_modules/array-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -2348,7 +2180,7 @@ "node_modules/array-initial": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", - "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", + "integrity": "sha512-BC4Yl89vneCYfpLrs5JU2aAu9/a+xWbeKhvISg9PT7eWFB9UlRvI+rKEtk6mgxWr3dSkk9gQ8hCrdqt06NXPdw==", "dev": true, "dependencies": { "array-slice": "^1.0.0", @@ -2358,15 +2190,6 @@ "node": ">=0.10.0" } }, - "node_modules/array-initial/node_modules/is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-last": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", @@ -2379,15 +2202,6 @@ "node": ">=0.10.0" } }, - "node_modules/array-last/node_modules/is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", @@ -2423,7 +2237,7 @@ "node_modules/array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", "dev": true, "engines": { "node": ">=0.10.0" @@ -2460,9 +2274,9 @@ } }, "node_modules/ast-types/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, "node_modules/async": { @@ -2486,15 +2300,21 @@ } }, "node_modules/async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] }, "node_modules/async-settle": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "integrity": "sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==", "dev": true, "dependencies": { "async-done": "^1.2.2" @@ -2545,15 +2365,15 @@ } }, "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", "dev": true }, "node_modules/bach": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", + "integrity": "sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==", "dev": true, "dependencies": { "arr-filter": "^1.1.1", @@ -2592,9 +2412,9 @@ "dev": true }, "node_modules/bare-events": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", - "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", "dev": true, "optional": true }, @@ -2658,7 +2478,7 @@ "node_modules/base/node_modules/define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", "dev": true, "dependencies": { "is-descriptor": "^1.0.0" @@ -2700,9 +2520,9 @@ } }, "node_modules/basic-ftp": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.3.tgz", - "integrity": "sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", "dev": true, "engines": { "node": ">=10.0.0" @@ -2729,6 +2549,16 @@ "url": "https://bevry.me/fund" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/blockly": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/blockly/-/blockly-10.0.2.tgz", @@ -2910,12 +2740,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2952,21 +2782,24 @@ } }, "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", "dev": true, "engines": { - "node": "*" + "node": ">=8.0.0" } }, "node_modules/buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", "dev": true, "engines": { - "node": ">=0.4.0" + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/buffer-from": { @@ -3044,13 +2877,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3065,6 +2904,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/chai": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", @@ -3178,7 +3026,7 @@ "node_modules/class-utils/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "dev": true, "dependencies": { "is-descriptor": "^0.1.0" @@ -3187,66 +3035,17 @@ "node": ">=0.10.0" } }, - "node_modules/class-utils/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dev": true, "dependencies": { - "kind-of": "^3.0.2" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/cliui": { @@ -3313,7 +3112,7 @@ "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -3322,7 +3121,7 @@ "node_modules/collection-map": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", - "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", + "integrity": "sha512-5D2XXSpkOnleOI21TG7p3T0bGAsZ/XknZpKBmGYyluO8pw4zA3K8ZlrBIbC4FXg3m6z/RNFiUFfT2sQK01+UHA==", "dev": true, "dependencies": { "arr-map": "^2.0.2", @@ -3336,7 +3135,7 @@ "node_modules/collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", "dev": true, "dependencies": { "map-visit": "^1.0.0", @@ -3403,10 +3202,13 @@ } }, "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/compress-commons": { "version": "6.0.2", @@ -3610,7 +3412,7 @@ "node_modules/copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", "dev": true, "engines": { "node": ">=0.10.0" @@ -3744,6 +3546,26 @@ "node-fetch": "^2.6.12" } }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3814,18 +3636,18 @@ } }, "node_modules/dat.gui": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.7.tgz", - "integrity": "sha512-sRl/28gF/XRC5ywC9I4zriATTsQcpSsRG7seXCPnTkK8/EQMIbCu5NPMpICLGxX9ZEUvcXR3ArLYCtgreFoMDw==", + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz", + "integrity": "sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==", "dev": true }, "node_modules/data-uri-to-buffer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-5.0.1.tgz", - "integrity": "sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "dev": true, "engines": { - "node": ">= 14" + "node": ">= 12" } }, "node_modules/data-urls": { @@ -3916,12 +3738,15 @@ } }, "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", + "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decimal.js": { @@ -4004,7 +3829,7 @@ "node_modules/default-resolution": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", - "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", + "integrity": "sha512-2xaP6GiwVwOEbXCGoJ4ufgC76m8cj805jrghScewJC2ZDsb9U0b4BIrba+xt/Uytyd0HvQ6+WymSRTfnYj59GQ==", "dev": true, "engines": { "node": ">= 0.10" @@ -4019,16 +3844,38 @@ "node": ">=10" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "dependencies": { - "object-keys": "^1.0.12" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-property": { @@ -4066,10 +3913,19 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", "dev": true, "engines": { "node": ">=0.10.0" @@ -4226,27 +4082,6 @@ "edgedriver": "bin/edgedriver.js" } }, - "node_modules/edgedriver/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/edgedriver/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/edgedriver/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -4256,24 +4091,6 @@ "node": ">=16" } }, - "node_modules/edgedriver/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dev": true, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/edgedriver/node_modules/which": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", @@ -4324,6 +4141,27 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", @@ -4331,14 +4169,19 @@ "dev": true }, "node_modules/es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, + "hasInstallScript": true, "dependencies": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" } }, "node_modules/es6-iterator": { @@ -4591,6 +4434,27 @@ "node": ">=10.13.0" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esniff/node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -4700,7 +4564,7 @@ "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", "dev": true, "dependencies": { "debug": "^2.3.3", @@ -4727,7 +4591,7 @@ "node_modules/expand-brackets/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "dev": true, "dependencies": { "is-descriptor": "^0.1.0" @@ -4739,7 +4603,7 @@ "node_modules/expand-brackets/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { "is-extendable": "^0.1.0" @@ -4748,72 +4612,23 @@ "node": ">=0.10.0" } }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/expand-brackets/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, "engines": { "node": ">=0.10.0" @@ -4822,13 +4637,13 @@ "node_modules/expand-brackets/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", "dev": true, "dependencies": { "homedir-polyfill": "^1.0.1" @@ -4893,7 +4708,7 @@ "node_modules/extglob/node_modules/define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", "dev": true, "dependencies": { "is-descriptor": "^1.0.0" @@ -4905,7 +4720,7 @@ "node_modules/extglob/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { "is-extendable": "^0.1.0" @@ -4917,7 +4732,7 @@ "node_modules/extglob/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, "engines": { "node": ">=0.10.0" @@ -4986,19 +4801,6 @@ "node": ">=8.6.0" } }, - "node_modules/fast-glob/node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -5029,7 +4831,7 @@ "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "dependencies": { "pend": "~1.2.0" @@ -5070,10 +4872,17 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -5107,19 +4916,6 @@ "micromatch": "^4.0.2" } }, - "node_modules/find-yarn-workspace-root/node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/findup-sync": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", @@ -5135,20 +4931,159 @@ "node": ">= 0.10" } }, - "node_modules/fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "node_modules/findup-sync/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, "dependencies": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/fined/node_modules/is-plain-object": { @@ -5261,9 +5196,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", - "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -5283,7 +5218,7 @@ "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", "dev": true, "engines": { "node": ">=0.10.0" @@ -5292,7 +5227,7 @@ "node_modules/for-own": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", "dev": true, "dependencies": { "for-in": "^1.0.1" @@ -5354,7 +5289,7 @@ "node_modules/fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", "dev": true, "dependencies": { "map-cache": "^0.2.2" @@ -5398,7 +5333,7 @@ "node_modules/fs-mkdirp-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", + "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", "dev": true, "dependencies": { "graceful-fs": "^4.1.11", @@ -5408,37 +5343,26 @@ "node": ">= 0.10" } }, - "node_modules/fs-mkdirp-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/fs-mkdirp-stream/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5449,9 +5373,9 @@ } }, "node_modules/geckodriver": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.4.1.tgz", - "integrity": "sha512-nnAdIrwLkMcDu4BitWXF23pEMeZZ0Cj7HaWWFdSpeedBP9z6ft150JYiGO2mwzw6UiR823Znk1JeIf07RyzloA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.4.2.tgz", + "integrity": "sha512-/JFJ7DJPJUvDhLjzQk+DwjlkAmiShddfRHhZ/xVL9FWbza5Bi3UMGmmerEKqD69JbRs7R81ZW31co686mdYZyA==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -5471,27 +5395,6 @@ "node": "^16.13 || >=18 || >=20" } }, - "node_modules/geckodriver/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/geckodriver/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/geckodriver/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -5501,22 +5404,14 @@ "node": ">=16" } }, - "node_modules/geckodriver/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "node_modules/geckodriver/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, "node_modules/geckodriver/node_modules/tar-fs": { @@ -5567,14 +5462,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5607,57 +5507,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-stream/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/get-uri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.1.tgz", - "integrity": "sha512-7ZqONUVqaabogsYNWlYj0t3YZaL6dhuEueZXGF+/YVmf6dHmaFg8/6psJKqhx9QykIDKzpGcy2cn4oV4YC7V/Q==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", "dev": true, "dependencies": { "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^5.0.1", + "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4", - "fs-extra": "^8.1.0" + "fs-extra": "^11.2.0" }, "engines": { "node": ">= 14" } }, - "node_modules/get-uri/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/get-uri/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "node": ">= 14" } }, - "node_modules/get-uri/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "node_modules/get-uri/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": ">= 4.0.0" + "node": ">=14.14" } }, "node_modules/get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -5700,7 +5601,7 @@ "node_modules/glob-stream": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", "dev": true, "dependencies": { "extend": "^3.0.0", @@ -5722,6 +5623,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -5741,7 +5643,7 @@ "node_modules/glob-stream/node_modules/glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", "dev": true, "dependencies": { "is-glob": "^3.1.0", @@ -5751,7 +5653,7 @@ "node_modules/glob-stream/node_modules/is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", "dev": true, "dependencies": { "is-extglob": "^2.1.0" @@ -5806,7 +5708,7 @@ "node_modules/glob-watcher/node_modules/anymatch/node_modules/normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", "dev": true, "dependencies": { "remove-trailing-separator": "^1.0.1" @@ -5860,11 +5762,23 @@ "node": ">=0.10.0" } }, + "node_modules/glob-watcher/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/glob-watcher/node_modules/chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "deprecated": "Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", "dev": true, "dependencies": { "anymatch": "^2.0.0", @@ -5883,10 +5797,25 @@ "fsevents": "^1.2.7" } }, - "node_modules/glob-watcher/node_modules/extend-shallow": { + "node_modules/glob-watcher/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/fill-range/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { "is-extendable": "^0.1.0" @@ -5895,25 +5824,29 @@ "node": ">=0.10.0" } }, - "node_modules/glob-watcher/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "node_modules/glob-watcher/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2", "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "bindings": "^1.5.0", + "nan": "^2.12.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 4.0" } }, "node_modules/glob-watcher/node_modules/glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", "dev": true, "dependencies": { "is-glob": "^3.1.0", @@ -5923,7 +5856,7 @@ "node_modules/glob-watcher/node_modules/glob-parent/node_modules/is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", "dev": true, "dependencies": { "is-extglob": "^2.1.0" @@ -5935,7 +5868,7 @@ "node_modules/glob-watcher/node_modules/is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", "dev": true, "dependencies": { "binary-extensions": "^1.0.0" @@ -5947,7 +5880,64 @@ "node_modules/glob-watcher/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/micromatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "engines": { "node": ">=0.10.0" @@ -5985,7 +5975,7 @@ "node_modules/glob-watcher/node_modules/to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", "dev": true, "dependencies": { "is-number": "^3.0.0", @@ -6036,7 +6026,7 @@ "node_modules/global-prefix": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", "dev": true, "dependencies": { "expand-tilde": "^2.0.2", @@ -6061,28 +6051,16 @@ "which": "bin/which" } }, - "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6193,6 +6171,18 @@ "win32" ] }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/got": { "version": "12.6.1", "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", @@ -6231,9 +6221,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, "node_modules/grapheme-splitter": { @@ -6310,10 +6300,10 @@ "node": ">=0.10.0" } }, - "node_modules/gulp-cli/node_modules/camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "node_modules/gulp-cli/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -6322,7 +6312,7 @@ "node_modules/gulp-cli/node_modules/cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", "dev": true, "dependencies": { "string-width": "^1.0.1", @@ -6330,15 +6320,11 @@ "wrap-ansi": "^2.0.0" } }, - "node_modules/gulp-cli/node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "node_modules/gulp-cli/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, "engines": { "node": ">=0.10.0" } @@ -6349,16 +6335,10 @@ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, - "node_modules/gulp-cli/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, "node_modules/gulp-cli/node_modules/is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", "dev": true, "dependencies": { "number-is-nan": "^1.0.0" @@ -6367,70 +6347,10 @@ "node": ">=0.10.0" } }, - "node_modules/gulp-cli/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/gulp-cli/node_modules/path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "dependencies": { - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-cli/node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-cli/node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-cli/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, "node_modules/gulp-cli/node_modules/string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", "dev": true, "dependencies": { "code-point-at": "^1.0.0", @@ -6444,7 +6364,7 @@ "node_modules/gulp-cli/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "dependencies": { "ansi-regex": "^2.0.0" @@ -6456,7 +6376,7 @@ "node_modules/gulp-cli/node_modules/wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", "dev": true, "dependencies": { "string-width": "^1.0.1", @@ -6517,31 +6437,6 @@ "node": ">= 0.10" } }, - "node_modules/gulp-concat/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/gulp-concat/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/gulp-gzip": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/gulp-gzip/-/gulp-gzip-1.4.2.tgz", @@ -6571,31 +6466,6 @@ "node": ">=0.10.0" } }, - "node_modules/gulp-gzip/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/gulp-gzip/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/gulp-header": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-2.0.9.tgz", @@ -6608,31 +6478,6 @@ "through2": "^2.0.0" } }, - "node_modules/gulp-header/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/gulp-header/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/gulp-insert": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/gulp-insert/-/gulp-insert-0.5.0.tgz", @@ -6772,21 +6617,6 @@ "node": ">=0.4.0" } }, - "node_modules/gulp-sourcemaps/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, "node_modules/gulp-sourcemaps/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6796,16 +6626,6 @@ "node": ">=0.10.0" } }, - "node_modules/gulp-sourcemaps/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/gulp-umd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/gulp-umd/-/gulp-umd-2.0.0.tgz", @@ -6817,35 +6637,10 @@ "through2": "^2.0.3" } }, - "node_modules/gulp-umd/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/gulp-umd/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/gulplog": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "integrity": "sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw==", "dev": true, "dependencies": { "glogg": "^1.0.0" @@ -6854,18 +6649,6 @@ "node": ">= 0.10" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -6875,10 +6658,34 @@ "node": ">=8" } }, - "node_modules/has-symbols": { + "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true, "engines": { "node": ">= 0.4" @@ -6890,7 +6697,7 @@ "node_modules/has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", "dev": true, "dependencies": { "get-value": "^2.0.6", @@ -6904,7 +6711,7 @@ "node_modules/has-values": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", "dev": true, "dependencies": { "is-number": "^3.0.0", @@ -6914,10 +6721,34 @@ "node": ">=0.10.0" } }, + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-values/node_modules/kind-of": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", "dev": true, "dependencies": { "is-buffer": "^1.1.5" @@ -6959,6 +6790,12 @@ "node": ">=0.10.0" } }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -7044,9 +6881,9 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -7179,16 +7016,29 @@ "node_modules/invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 12" } }, - "node_modules/ip": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", - "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, "node_modules/is-absolute": { @@ -7205,30 +7055,21 @@ } }, "node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", "dev": true, "dependencies": { - "kind-of": "^6.0.0" + "hasown": "^2.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, "node_modules/is-binary-path": { @@ -7262,47 +7103,28 @@ } }, "node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", "dev": true, "dependencies": { - "kind-of": "^6.0.0" + "hasown": "^2.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/is-docker": { @@ -7377,32 +7199,17 @@ "node_modules/is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, "engines": { "node": ">=0.10.0" } @@ -7416,6 +7223,18 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -7487,13 +7306,13 @@ "node_modules/is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", "dev": true }, "node_modules/is-valid-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -7593,6 +7412,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", @@ -7877,7 +7702,7 @@ "node_modules/last-run": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", + "integrity": "sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ==", "dev": true, "dependencies": { "default-resolution": "^2.0.0", @@ -7917,7 +7742,7 @@ "node_modules/lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", "dev": true, "dependencies": { "invert-kv": "^1.0.0" @@ -7929,7 +7754,7 @@ "node_modules/lead": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", + "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", "dev": true, "dependencies": { "flush-write-stream": "^1.0.2" @@ -7994,7 +7819,7 @@ "node_modules/load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", "dev": true, "dependencies": { "graceful-fs": "^4.1.2", @@ -8007,18 +7832,6 @@ "node": ">=0.10.0" } }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/locate-app": { "version": "2.4.21", "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.4.21.tgz", @@ -8040,6 +7853,18 @@ "userhome": "1.0.0" } }, + "node_modules/locate-app/node_modules/type-fest": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.13.0.tgz", + "integrity": "sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8217,7 +8042,7 @@ "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -8232,7 +8057,7 @@ "node_modules/map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", "dev": true, "dependencies": { "object-visit": "^1.0.0" @@ -8253,7 +8078,7 @@ "node_modules/matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", + "integrity": "sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA==", "dev": true, "dependencies": { "findup-sync": "^2.0.0", @@ -8265,10 +8090,70 @@ "node": ">= 0.10.0" } }, + "node_modules/matchdep/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/matchdep/node_modules/findup-sync": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "integrity": "sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==", "dev": true, "dependencies": { "detect-file": "^1.0.0", @@ -8280,10 +8165,19 @@ "node": ">= 0.10" } }, + "node_modules/matchdep/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/matchdep/node_modules/is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", "dev": true, "dependencies": { "is-extglob": "^2.1.0" @@ -8292,38 +8186,40 @@ "node": ">=0.10.0" } }, - "node_modules/memoizee": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", + "node_modules/matchdep/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "dev": true, "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/memoizee/node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true + "node_modules/matchdep/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/matchdep/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "engines": { - "node": ">= 8" + "node": ">=0.10.0" } }, - "node_modules/micromatch": { + "node_modules/matchdep/node_modules/micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", @@ -8347,95 +8243,55 @@ "node": ">=0.10.0" } }, - "node_modules/micromatch/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "node_modules/matchdep/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", "dev": true, "dependencies": { - "extend-shallow": "^2.0.1", "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "repeat-string": "^1.6.1" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/micromatch/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "node_modules/memoizee": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", + "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", "dev": true, "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true, - "engines": { - "node": ">=0.10.0" + "d": "^1.0.1", + "es5-ext": "^0.10.53", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" } }, - "node_modules/micromatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, - "node_modules/micromatch/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dev": true, "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.6" } }, "node_modules/mime": { @@ -8725,6 +8581,13 @@ "node": ">= 0.10" } }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "dev": true, + "optional": true + }, "node_modules/nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -8784,9 +8647,9 @@ } }, "node_modules/next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, "node_modules/nise": { @@ -8822,23 +8685,42 @@ } }, "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dev": true, "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" } }, "node_modules/normalize-path": { @@ -8877,7 +8759,7 @@ "node_modules/number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", "dev": true, "engines": { "node": ">=0.10.0" @@ -8900,7 +8782,7 @@ "node_modules/object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", "dev": true, "dependencies": { "copy-descriptor": "^0.1.0", @@ -8914,7 +8796,7 @@ "node_modules/object-copy/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "dev": true, "dependencies": { "is-descriptor": "^0.1.0" @@ -8923,57 +8805,23 @@ "node": ">=0.10.0" } }, - "node_modules/object-copy/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/object-copy/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { "is-buffer": "^1.1.5" @@ -8994,7 +8842,7 @@ "node_modules/object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", "dev": true, "dependencies": { "isobject": "^3.0.0" @@ -9004,14 +8852,14 @@ } }, "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, "engines": { @@ -9024,7 +8872,7 @@ "node_modules/object.defaults": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", "dev": true, "dependencies": { "array-each": "^1.0.1", @@ -9039,7 +8887,7 @@ "node_modules/object.map": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", "dev": true, "dependencies": { "for-own": "^1.0.0", @@ -9052,7 +8900,7 @@ "node_modules/object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", "dev": true, "dependencies": { "isobject": "^3.0.1" @@ -9064,7 +8912,7 @@ "node_modules/object.reduce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", - "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", + "integrity": "sha512-naLhxxpUESbNkRqc35oQ2scZSJueHGQNUfMW/0U37IgN6tE2dgDWg3whf+NEliy3F/QysrO48XKUz/nGPe+AQw==", "dev": true, "dependencies": { "for-own": "^1.0.0", @@ -9128,7 +8976,7 @@ "node_modules/ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", "dev": true, "dependencies": { "readable-stream": "^2.0.1" @@ -9152,7 +9000,7 @@ "node_modules/os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", "dev": true, "dependencies": { "lcid": "^1.0.0" @@ -9210,9 +9058,9 @@ } }, "node_modules/pac-proxy-agent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", - "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", "dev": true, "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", @@ -9220,22 +9068,21 @@ "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", - "pac-resolver": "^7.0.0", - "socks-proxy-agent": "^8.0.2" + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" }, "engines": { "node": ">= 14" } }, "node_modules/pac-resolver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.0.tgz", - "integrity": "sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, "dependencies": { "degenerator": "^5.0.0", - "ip": "^1.1.8", "netmask": "^2.0.2" }, "engines": { @@ -9263,7 +9110,7 @@ "node_modules/parse-filepath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", "dev": true, "dependencies": { "is-absolute": "^1.0.0", @@ -9287,6 +9134,18 @@ "node": ">= 18" } }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -9299,7 +9158,7 @@ "node_modules/parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", "dev": true, "engines": { "node": ">=0.10.0" @@ -9319,7 +9178,7 @@ "node_modules/pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", "dev": true, "engines": { "node": ">=0.10.0" @@ -9414,7 +9273,7 @@ "node_modules/path-dirname": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", "dev": true }, "node_modules/path-exists": { @@ -9453,7 +9312,7 @@ "node_modules/path-root": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", "dev": true, "dependencies": { "path-root-regex": "^0.1.0" @@ -9465,7 +9324,7 @@ "node_modules/path-root-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", "dev": true, "engines": { "node": ">=0.10.0" @@ -9514,7 +9373,7 @@ "node_modules/path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", "dev": true, "dependencies": { "graceful-fs": "^4.1.2", @@ -9537,7 +9396,7 @@ "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, "node_modules/picocolors": { @@ -9561,7 +9420,7 @@ "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "engines": { "node": ">=0.10.0" @@ -9570,7 +9429,7 @@ "node_modules/pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -9579,7 +9438,7 @@ "node_modules/pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, "dependencies": { "pinkie": "^2.0.0" @@ -9650,7 +9509,7 @@ "node_modules/posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -9709,7 +9568,7 @@ "node_modules/pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", "dev": true, "engines": { "node": ">= 0.8" @@ -9740,19 +9599,19 @@ } }, "node_modules/proxy-agent": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", - "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", "dev": true, "dependencies": { "agent-base": "^7.0.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.0", + "pac-proxy-agent": "^7.0.1", "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.1" + "socks-proxy-agent": "^8.0.2" }, "engines": { "node": ">= 14" @@ -9779,9 +9638,9 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "dev": true, "dependencies": { "end-of-stream": "^1.1.0", @@ -9799,16 +9658,6 @@ "pump": "^2.0.0" } }, - "node_modules/pumpify/node_modules/pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9842,12 +9691,101 @@ } } }, + "node_modules/puppeteer-core/node_modules/@puppeteer/browsers": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.4.6.tgz", + "integrity": "sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==", + "dev": true, + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.3.0", + "tar-fs": "3.0.4", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=16.3.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/puppeteer-core/node_modules/devtools-protocol": { "version": "0.0.1147663", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz", "integrity": "sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==", "dev": true }, + "node_modules/puppeteer-core/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/puppeteer-core/node_modules/proxy-agent": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", + "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/puppeteer-core/node_modules/yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/qs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", @@ -9894,25 +9832,77 @@ "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", "dev": true }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "dev": true, + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", "dev": true, + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, "engines": { - "node": ">=10" + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "dev": true, + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/randombytes": { + "node_modules/read-pkg-up/node_modules/path-exists": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", "dev": true, "dependencies": { - "safe-buffer": "^5.1.0" + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/readable-stream": { @@ -9983,7 +9973,7 @@ "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", "dev": true, "dependencies": { "resolve": "^1.1.6" @@ -10027,7 +10017,7 @@ "node_modules/remove-bom-stream": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", - "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", + "integrity": "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA==", "dev": true, "dependencies": { "remove-bom-buffer": "^3.0.0", @@ -10038,31 +10028,6 @@ "node": ">= 0.10" } }, - "node_modules/remove-bom-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/remove-bom-stream/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -10081,7 +10046,7 @@ "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", "dev": true, "engines": { "node": ">=0.10" @@ -10090,7 +10055,7 @@ "node_modules/replace-homedir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", - "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", + "integrity": "sha512-CHPV/GAglbIB1tnQgaiysb8H2yCy8WQ7lcEwQ/eT+kLj0QHV8LnJW0zpqpE7RSkrMSRoa+EBoag86clf7WAgSg==", "dev": true, "dependencies": { "homedir-polyfill": "^1.0.1", @@ -10157,7 +10122,7 @@ "node_modules/require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", "dev": true }, "node_modules/requires-port": { @@ -10191,7 +10156,7 @@ "node_modules/resolve-dir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", "dev": true, "dependencies": { "expand-tilde": "^2.0.0", @@ -10213,7 +10178,7 @@ "node_modules/resolve-options": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", - "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", + "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", "dev": true, "dependencies": { "value-or-function": "^3.0.0" @@ -10225,7 +10190,7 @@ "node_modules/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", "deprecated": "https://github.com/lydell/resolve-url#deprecated", "dev": true }, @@ -10362,7 +10327,7 @@ "node_modules/safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", "dev": true, "dependencies": { "ret": "~0.1.10" @@ -10408,7 +10373,7 @@ "node_modules/semver-greatest-satisfied-range": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", - "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", + "integrity": "sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ==", "dev": true, "dependencies": { "sver-compat": "^1.5.0" @@ -10418,9 +10383,9 @@ } }, "node_modules/serialize-error": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.2.tgz", - "integrity": "sha512-o43i0jLcA0LXA5Uu+gI1Vj+lF66KR9IAcy0ThbGq1bAMPN+k5IgSHsulfnqf/ddKAz6dWf+k8PD5hAr9oCSHEQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", + "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", "dev": true, "dependencies": { "type-fest": "^2.12.2" @@ -10432,6 +10397,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -10444,9 +10421,26 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -10465,7 +10459,7 @@ "node_modules/set-value/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { "is-extendable": "^0.1.0" @@ -10477,7 +10471,7 @@ "node_modules/set-value/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10631,7 +10625,7 @@ "node_modules/snapdragon-node/node_modules/define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", "dev": true, "dependencies": { "is-descriptor": "^1.0.0" @@ -10655,7 +10649,7 @@ "node_modules/snapdragon-util/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { "is-buffer": "^1.1.5" @@ -10676,7 +10670,7 @@ "node_modules/snapdragon/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "dev": true, "dependencies": { "is-descriptor": "^0.1.0" @@ -10688,7 +10682,7 @@ "node_modules/snapdragon/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { "is-extendable": "^0.1.0" @@ -10697,72 +10691,23 @@ "node": ">=0.10.0" } }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/snapdragon/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10771,13 +10716,14 @@ "node_modules/snapdragon/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, "node_modules/snapdragon/node_modules/source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", "dev": true, "dependencies": { "atob": "^2.1.2", @@ -10788,39 +10734,33 @@ } }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "dev": true, "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks-proxy-agent": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", - "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", "dev": true, "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.1", "debug": "^4.3.4", - "socks": "^2.7.1" + "socks": "^2.8.3" }, "engines": { "node": ">= 14" } }, - "node_modules/socks/node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "dev": true - }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -10834,6 +10774,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", "dev": true, "dependencies": { "atob": "^2.1.2", @@ -10844,6 +10785,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", "dev": true }, "node_modules/spacetrim": { @@ -10878,9 +10820,9 @@ "dev": true }, "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -10939,7 +10881,7 @@ "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", "dev": true, "engines": { "node": "*" @@ -10948,7 +10890,7 @@ "node_modules/static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", "dev": true, "dependencies": { "define-property": "^0.2.5", @@ -10961,7 +10903,7 @@ "node_modules/static-extend/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "dev": true, "dependencies": { "is-descriptor": "^0.1.0" @@ -10970,66 +10912,17 @@ "node": ">=0.10.0" } }, - "node_modules/static-extend/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/stream-exhaust": { @@ -11039,9 +10932,9 @@ "dev": true }, "node_modules/stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "dev": true }, "node_modules/stream-to-array": { @@ -11196,7 +11089,7 @@ "node_modules/strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", "dev": true, "dependencies": { "is-utf8": "^0.2.0" @@ -11253,7 +11146,7 @@ "node_modules/sver-compat": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", - "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", + "integrity": "sha512-aFTHfmjwizMNlNE6dsGmoAM4lHjL0CyiobWaFiXWSlD7cIxshW422Nb8KbXCmR6z+0ZEPY+daXJrDyh/vuwTyg==", "dev": true, "dependencies": { "es6-iterator": "^2.0.1", @@ -11298,10 +11191,20 @@ "tar-stream": "^3.1.5" } }, + "node_modules/tar-fs/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, "dependencies": { "b4a": "^1.6.4", @@ -11310,9 +11213,9 @@ } }, "node_modules/text-decoder": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", - "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", + "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", "dev": true, "dependencies": { "b4a": "^1.6.4" @@ -11342,6 +11245,16 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/through2-filter": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", @@ -11352,7 +11265,7 @@ "xtend": "~4.0.0" } }, - "node_modules/through2-filter/node_modules/readable-stream": { + "node_modules/through2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", @@ -11367,16 +11280,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/through2-filter/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/time-stamp": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", @@ -11411,7 +11314,7 @@ "node_modules/to-absolute-glob": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", "dev": true, "dependencies": { "is-absolute": "^1.0.0", @@ -11424,7 +11327,7 @@ "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", "dev": true, "dependencies": { "kind-of": "^3.0.2" @@ -11436,7 +11339,7 @@ "node_modules/to-object-path/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { "is-buffer": "^1.1.5" @@ -11484,7 +11387,7 @@ "node_modules/to-through": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", + "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", "dev": true, "dependencies": { "through2": "^2.0.3" @@ -11493,31 +11396,6 @@ "node": ">= 0.10" } }, - "node_modules/to-through/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/to-through/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -11601,12 +11479,12 @@ } }, "node_modules/type-fest": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.13.0.tgz", - "integrity": "sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "engines": { - "node": ">=12.20" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -11644,7 +11522,7 @@ "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -11674,7 +11552,7 @@ "node_modules/undertaker-registry": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", + "integrity": "sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw==", "dev": true, "engines": { "node": ">= 0.10" @@ -11683,13 +11561,13 @@ "node_modules/undertaker/node_modules/fast-levenshtein": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", - "integrity": "sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk=", + "integrity": "sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw==", "dev": true }, "node_modules/undici": { - "version": "5.28.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", - "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", "dev": true, "dependencies": { "@fastify/busboy": "^2.0.0" @@ -11728,7 +11606,7 @@ "node_modules/union-value/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, "engines": { "node": ">=0.10.0" @@ -11756,7 +11634,7 @@ "node_modules/unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", "dev": true, "dependencies": { "has-value": "^0.3.1", @@ -11769,7 +11647,7 @@ "node_modules/unset-value/node_modules/has-value": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", "dev": true, "dependencies": { "get-value": "^2.0.3", @@ -11783,7 +11661,7 @@ "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", "dev": true, "dependencies": { "isarray": "1.0.0" @@ -11795,7 +11673,7 @@ "node_modules/unset-value/node_modules/has-values": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", "dev": true, "engines": { "node": ">=0.10.0" @@ -11823,7 +11701,7 @@ "node_modules/urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", "deprecated": "Please see https://github.com/lydell/urix#deprecated", "dev": true }, @@ -11900,7 +11778,7 @@ "node_modules/value-or-function": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", - "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", + "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", "dev": true, "engines": { "node": ">= 0.10" @@ -11966,20 +11844,10 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/vinyl-fs/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/vinyl-sourcemap": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", - "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", + "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", "dev": true, "dependencies": { "append-buffer": "^1.0.2", @@ -11997,7 +11865,7 @@ "node_modules/vinyl-sourcemap/node_modules/normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", "dev": true, "dependencies": { "remove-trailing-separator": "^1.0.1" @@ -12084,9 +11952,9 @@ } }, "node_modules/webdriverio": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.39.0.tgz", - "integrity": "sha512-pDpGu0V+TL1LkXPode67m3s+IPto4TcmcOzMpzFgu2oeLMBornoLN3yQSFR1fjZd1gK4UfnG3lJ4poTGOfbWfw==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.39.1.tgz", + "integrity": "sha512-dPwLgLNtP+l4vnybz+YFxxH8nBKOP7j6VVzKtfDyTLDQg9rz3U8OA4xMMQCBucnrVXy3KcKxGqlnMa+c4IfWCQ==", "dev": true, "dependencies": { "@types/node": "^20.1.0", @@ -12136,22 +12004,10 @@ "balanced-match": "^1.0.0" } }, - "node_modules/webdriverio/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/webdriverio/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -12225,7 +12081,7 @@ "node_modules/which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", "dev": true }, "node_modules/workerpool": { @@ -12434,13 +12290,22 @@ "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, + "node_modules/yauzl/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", From a2a57496940383c40f3b4d60c6a4a68204f6abce Mon Sep 17 00:00:00 2001 From: Shreyans Pathak Date: Wed, 17 Jul 2024 13:24:09 -0400 Subject: [PATCH 018/222] feat: Add css classes from json block definitions (#8377) * fix: override `jsonInit` method to add css classes * fix: lint * refactor: simplify logic --- core/block_svg.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/block_svg.ts b/core/block_svg.ts index adef213d5fc..a5b7f8d1049 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -1720,4 +1720,16 @@ export class BlockSvg traverseJson(json as unknown as {[key: string]: unknown}); return [json]; } + + override jsonInit(json: AnyDuringMigration): void { + super.jsonInit(json); + + if (json['classes']) { + this.addClass( + Array.isArray(json['classes']) + ? json['classes'].join(' ') + : json['classes'], + ); + } + } } From e1753ae066395f799d7929df4e23204ec3944096 Mon Sep 17 00:00:00 2001 From: Ruthwik Chikoti <145591715+ruthwikchikoti@users.noreply.github.com> Date: Wed, 17 Jul 2024 23:08:29 +0530 Subject: [PATCH 019/222] fix!: Renamed the blocklyToolboxContents CSS class to blocklyToolboxCategoryGroup (#8384) --- core/toolbox/category.ts | 2 +- core/toolbox/collapsible_category.ts | 2 +- core/toolbox/toolbox.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/toolbox/category.ts b/core/toolbox/category.ts index bbcb6bf6ff1..b47ba657cff 100644 --- a/core/toolbox/category.ts +++ b/core/toolbox/category.ts @@ -136,7 +136,7 @@ export class ToolboxCategory 'rowcontentcontainer': 'blocklyTreeRowContentContainer', 'icon': 'blocklyToolboxCategoryIcon', 'label': 'blocklyTreeLabel', - 'contents': 'blocklyToolboxContents', + 'contents': 'blocklyToolboxCategoryGroup', 'selected': 'blocklyTreeSelected', 'openicon': 'blocklyTreeIconOpen', 'closedicon': 'blocklyTreeIconClosed', diff --git a/core/toolbox/collapsible_category.ts b/core/toolbox/collapsible_category.ts index faea8edcb80..1a30a1c8108 100644 --- a/core/toolbox/collapsible_category.ts +++ b/core/toolbox/collapsible_category.ts @@ -58,7 +58,7 @@ export class CollapsibleToolboxCategory override makeDefaultCssConfig_() { const cssConfig = super.makeDefaultCssConfig_(); - cssConfig['contents'] = 'blocklyToolboxContents'; + cssConfig['contents'] = 'blocklyToolboxCategoryGroup'; return cssConfig; } diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index e0fb62e23da..ac4a7da91b8 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -209,7 +209,7 @@ export class Toolbox */ protected createContentsContainer_(): HTMLDivElement { const contentsContainer = document.createElement('div'); - dom.addClass(contentsContainer, 'blocklyToolboxContents'); + dom.addClass(contentsContainer, 'blocklyToolboxCategoryGroup'); if (this.isHorizontal()) { contentsContainer.style.flexDirection = 'row'; } @@ -1111,13 +1111,13 @@ Css.register(` -webkit-tap-highlight-color: transparent; /* issue #1345 */ } -.blocklyToolboxContents { +.blocklyToolboxCategoryGroup { display: flex; flex-wrap: wrap; flex-direction: column; } -.blocklyToolboxContents:focus { +.blocklyToolboxCategoryGroup:focus { outline: none; } `); From 0a1524f57702e90bfc5e772812e9a2bd94e1eb6c Mon Sep 17 00:00:00 2001 From: Suryansh Shakya <83297944+nullHawk@users.noreply.github.com> Date: Wed, 17 Jul 2024 23:15:11 +0530 Subject: [PATCH 020/222] feat: added blocklyToolboxFlyout CSS class to the flyout (#8386) --- core/toolbox/toolbox.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index ac4a7da91b8..1e2a5970f61 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -143,7 +143,9 @@ export class Toolbox this.flyout_ = this.createFlyout_(); this.HtmlDiv = this.createDom_(this.workspace_); - dom.insertAfter(this.flyout_.createDom('svg'), svg); + const flyoutDom = this.flyout_.createDom('svg'); + dom.addClass(flyoutDom, 'blocklyToolboxFlyout'); + dom.insertAfter(flyoutDom, svg); this.setVisible(true); this.flyout_.init(workspace); From 9fa4b2c9664bd50017f5a971eefbf6fcbdb5a20e Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Thu, 18 Jul 2024 17:22:25 +0000 Subject: [PATCH 021/222] chore: fix package-lock --- package-lock.json | 2779 ++++++++++++++++++++++++--------------------- 1 file changed, 1457 insertions(+), 1322 deletions(-) diff --git a/package-lock.json b/package-lock.json index c809034ebaf..a5249e6b187 100644 --- a/package-lock.json +++ b/package-lock.json @@ -397,6 +397,31 @@ "node": ">=0.10.0" } }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -506,6 +531,18 @@ "node": ">=12" } }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", @@ -799,24 +836,64 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", - "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.4.6.tgz", + "integrity": "sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==", "dev": true, "dependencies": { "debug": "4.3.4", "extract-zip": "2.0.1", "progress": "2.0.3", - "proxy-agent": "6.3.1", + "proxy-agent": "6.3.0", "tar-fs": "3.0.4", "unbzip2-stream": "1.4.3", - "yargs": "17.7.2" + "yargs": "17.7.1" }, "bin": { "browsers": "lib/cjs/main-cli.js" }, "engines": { "node": ">=16.3.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" } }, "node_modules/@rushstack/node-core-library": { @@ -1096,18 +1173,18 @@ "dev": true }, "node_modules/@types/ws": { - "version": "8.5.11", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", - "integrity": "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", "dev": true, "optional": true, "dependencies": { @@ -1605,6 +1682,18 @@ "node": "^16.13 || >=18" } }, + "node_modules/@wdio/config/node_modules/decamelize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", + "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@wdio/logger": { "version": "8.38.0", "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", @@ -1620,6 +1709,18 @@ "node": "^16.13 || >=18" } }, + "node_modules/@wdio/logger/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/@wdio/logger/node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -1701,6 +1802,67 @@ "node": "^16.13 || >=18" } }, + "node_modules/@wdio/utils/node_modules/@puppeteer/browsers": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", + "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", + "dev": true, + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.3.1", + "tar-fs": "3.0.4", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=16.3.0" + } + }, + "node_modules/@wdio/utils/node_modules/decamelize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", + "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@wdio/utils/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@wdio/utils/node_modules/proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -1760,9 +1922,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", "dependencies": { "debug": "^4.3.4" }, @@ -1847,15 +2009,12 @@ } }, "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=0.10.0" } }, "node_modules/ansi-styles": { @@ -1889,9 +2048,9 @@ "dev": true }, "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, "dependencies": { "normalize-path": "^3.0.0", @@ -1904,7 +2063,7 @@ "node_modules/append-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", - "integrity": "sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA==", + "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", "dev": true, "dependencies": { "buffer-equal": "^1.0.0" @@ -2042,6 +2201,15 @@ "ieee754": "^1.2.1" } }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/archiver/node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -2090,7 +2258,7 @@ "node_modules/archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, "node_modules/are-docs-informative": { @@ -2109,12 +2277,12 @@ "dev": true }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", + "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "engines": { + "node": ">=6.0" } }, "node_modules/arr-diff": { @@ -2129,7 +2297,7 @@ "node_modules/arr-filter": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", - "integrity": "sha512-A2BETWCqhsecSvCkWAeVBFLH6sXEUGASuzkpjL3GR1SlL/PWL6M3J8EAAld2Uubmh39tvkJTqC9LeLHCUKmFXA==", + "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", "dev": true, "dependencies": { "make-iterator": "^1.0.0" @@ -2150,7 +2318,7 @@ "node_modules/arr-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", - "integrity": "sha512-tVqVTHt+Q5Xb09qRkbu+DidW1yYzz5izWS2Xm2yFm7qJnmUfz4HPzNxbHkdRJbz2lrqI7S+z17xNYdFcBBO8Hw==", + "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", "dev": true, "dependencies": { "make-iterator": "^1.0.0" @@ -2171,7 +2339,7 @@ "node_modules/array-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", "dev": true, "engines": { "node": ">=0.10.0" @@ -2180,7 +2348,7 @@ "node_modules/array-initial": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", - "integrity": "sha512-BC4Yl89vneCYfpLrs5JU2aAu9/a+xWbeKhvISg9PT7eWFB9UlRvI+rKEtk6mgxWr3dSkk9gQ8hCrdqt06NXPdw==", + "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", "dev": true, "dependencies": { "array-slice": "^1.0.0", @@ -2190,6 +2358,15 @@ "node": ">=0.10.0" } }, + "node_modules/array-initial/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-last": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", @@ -2202,6 +2379,15 @@ "node": ">=0.10.0" } }, + "node_modules/array-last/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", @@ -2237,7 +2423,7 @@ "node_modules/array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true, "engines": { "node": ">=0.10.0" @@ -2274,9 +2460,9 @@ } }, "node_modules/ast-types/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, "node_modules/async": { @@ -2300,21 +2486,15 @@ } }, "node_modules/async-each": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", - "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true }, "node_modules/async-settle": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==", + "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", "dev": true, "dependencies": { "async-done": "^1.2.2" @@ -2365,15 +2545,15 @@ } }, "node_modules/b4a": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", + "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==", "dev": true }, "node_modules/bach": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==", + "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", "dev": true, "dependencies": { "arr-filter": "^1.1.1", @@ -2412,9 +2592,9 @@ "dev": true }, "node_modules/bare-events": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", - "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", + "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", "dev": true, "optional": true }, @@ -2478,7 +2658,7 @@ "node_modules/base/node_modules/define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "dependencies": { "is-descriptor": "^1.0.0" @@ -2520,9 +2700,9 @@ } }, "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.3.tgz", + "integrity": "sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==", "dev": true, "engines": { "node": ">=10.0.0" @@ -2549,16 +2729,6 @@ "url": "https://bevry.me/fund" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/blockly": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/blockly/-/blockly-10.0.2.tgz", @@ -2740,12 +2910,12 @@ } }, "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, "dependencies": { - "fill-range": "^7.1.1" + "fill-range": "^7.0.1" }, "engines": { "node": ">=8" @@ -2782,24 +2952,21 @@ } }, "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", "dev": true, "engines": { - "node": ">=8.0.0" + "node": "*" } }, "node_modules/buffer-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", - "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", "dev": true, "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.4.0" } }, "node_modules/buffer-from": { @@ -2877,19 +3044,13 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "dev": true, "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2904,15 +3065,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/chai": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", @@ -3026,7 +3178,7 @@ "node_modules/class-utils/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "dependencies": { "is-descriptor": "^0.1.0" @@ -3035,17 +3187,66 @@ "node": ">=0.10.0" } }, - "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "node_modules/class-utils/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "kind-of": "^3.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/cliui": { @@ -3112,7 +3313,7 @@ "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true, "engines": { "node": ">=0.10.0" @@ -3121,7 +3322,7 @@ "node_modules/collection-map": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", - "integrity": "sha512-5D2XXSpkOnleOI21TG7p3T0bGAsZ/XknZpKBmGYyluO8pw4zA3K8ZlrBIbC4FXg3m6z/RNFiUFfT2sQK01+UHA==", + "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", "dev": true, "dependencies": { "arr-map": "^2.0.2", @@ -3135,7 +3336,7 @@ "node_modules/collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", "dev": true, "dependencies": { "map-visit": "^1.0.0", @@ -3202,13 +3403,10 @@ } }, "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true }, "node_modules/compress-commons": { "version": "6.0.2", @@ -3412,7 +3610,7 @@ "node_modules/copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true, "engines": { "node": ">=0.10.0" @@ -3546,26 +3744,6 @@ "node-fetch": "^2.6.12" } }, - "node_modules/cross-fetch/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3636,18 +3814,18 @@ } }, "node_modules/dat.gui": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz", - "integrity": "sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==", + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.7.tgz", + "integrity": "sha512-sRl/28gF/XRC5ywC9I4zriATTsQcpSsRG7seXCPnTkK8/EQMIbCu5NPMpICLGxX9ZEUvcXR3ArLYCtgreFoMDw==", "dev": true }, "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-5.0.1.tgz", + "integrity": "sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==", "dev": true, "engines": { - "node": ">= 12" + "node": ">= 14" } }, "node_modules/data-urls": { @@ -3738,15 +3916,12 @@ } }, "node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/decimal.js": { @@ -3829,7 +4004,7 @@ "node_modules/default-resolution": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", - "integrity": "sha512-2xaP6GiwVwOEbXCGoJ4ufgC76m8cj805jrghScewJC2ZDsb9U0b4BIrba+xt/Uytyd0HvQ6+WymSRTfnYj59GQ==", + "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", "dev": true, "engines": { "node": ">= 0.10" @@ -3844,38 +4019,16 @@ "node": ">=10" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "object-keys": "^1.0.12" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-property": { @@ -3913,19 +4066,10 @@ "node": ">=0.4.0" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", "dev": true, "engines": { "node": ">=0.10.0" @@ -4082,6 +4226,27 @@ "edgedriver": "bin/edgedriver.js" } }, + "node_modules/edgedriver/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/edgedriver/node_modules/decamelize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", + "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/edgedriver/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -4091,6 +4256,24 @@ "node": ">=16" } }, + "node_modules/edgedriver/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/edgedriver/node_modules/which": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", @@ -4141,27 +4324,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", @@ -4169,19 +4331,14 @@ "dev": true }, "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", "dev": true, - "hasInstallScript": true, "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" } }, "node_modules/es6-iterator": { @@ -4434,27 +4591,6 @@ "node": ">=10.13.0" } }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "dev": true, - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esniff/node_modules/type": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "dev": true - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -4564,7 +4700,7 @@ "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", "dev": true, "dependencies": { "debug": "^2.3.3", @@ -4591,7 +4727,7 @@ "node_modules/expand-brackets/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "dependencies": { "is-descriptor": "^0.1.0" @@ -4603,7 +4739,7 @@ "node_modules/expand-brackets/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "dependencies": { "is-extendable": "^0.1.0" @@ -4612,23 +4748,72 @@ "node": ">=0.10.0" } }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, "node_modules/expand-brackets/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "dev": true, "engines": { "node": ">=0.10.0" @@ -4637,13 +4822,13 @@ "node_modules/expand-brackets/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", "dev": true, "dependencies": { "homedir-polyfill": "^1.0.1" @@ -4708,7 +4893,7 @@ "node_modules/extglob/node_modules/define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "dependencies": { "is-descriptor": "^1.0.0" @@ -4720,7 +4905,7 @@ "node_modules/extglob/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "dependencies": { "is-extendable": "^0.1.0" @@ -4732,7 +4917,7 @@ "node_modules/extglob/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "dev": true, "engines": { "node": ">=0.10.0" @@ -4801,6 +4986,19 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4831,7 +5029,7 @@ "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "dev": true, "dependencies": { "pend": "~1.2.0" @@ -4872,17 +5070,10 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -4916,6 +5107,19 @@ "micromatch": "^4.0.2" } }, + "node_modules/find-yarn-workspace-root/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/findup-sync": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", @@ -4931,159 +5135,20 @@ "node": ">= 0.10" } }, - "node_modules/findup-sync/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", "dev": true, "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", - "dev": true, - "dependencies": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" + "node": ">= 0.10" } }, "node_modules/fined/node_modules/is-plain-object": { @@ -5196,9 +5261,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "dev": true, "funding": [ { @@ -5218,7 +5283,7 @@ "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "dev": true, "engines": { "node": ">=0.10.0" @@ -5227,7 +5292,7 @@ "node_modules/for-own": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", "dev": true, "dependencies": { "for-in": "^1.0.1" @@ -5289,7 +5354,7 @@ "node_modules/fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", "dev": true, "dependencies": { "map-cache": "^0.2.2" @@ -5333,7 +5398,7 @@ "node_modules/fs-mkdirp-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", + "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", "dev": true, "dependencies": { "graceful-fs": "^4.1.11", @@ -5343,26 +5408,37 @@ "node": ">= 0.10" } }, + "node_modules/fs-mkdirp-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/fs-mkdirp-stream/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5373,9 +5449,9 @@ } }, "node_modules/geckodriver": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.4.2.tgz", - "integrity": "sha512-/JFJ7DJPJUvDhLjzQk+DwjlkAmiShddfRHhZ/xVL9FWbza5Bi3UMGmmerEKqD69JbRs7R81ZW31co686mdYZyA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.4.1.tgz", + "integrity": "sha512-nnAdIrwLkMcDu4BitWXF23pEMeZZ0Cj7HaWWFdSpeedBP9z6ft150JYiGO2mwzw6UiR823Znk1JeIf07RyzloA==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -5395,6 +5471,27 @@ "node": "^16.13 || >=18 || >=20" } }, + "node_modules/geckodriver/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/geckodriver/node_modules/decamelize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", + "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/geckodriver/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -5404,14 +5501,22 @@ "node": ">=16" } }, - "node_modules/geckodriver/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "node_modules/geckodriver/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dev": true, "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "node_modules/geckodriver/node_modules/tar-fs": { @@ -5462,19 +5567,14 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "dev": true, "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5507,58 +5607,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-stream/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/get-uri": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", - "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.1.tgz", + "integrity": "sha512-7ZqONUVqaabogsYNWlYj0t3YZaL6dhuEueZXGF+/YVmf6dHmaFg8/6psJKqhx9QykIDKzpGcy2cn4oV4YC7V/Q==", "dev": true, "dependencies": { "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", + "data-uri-to-buffer": "^5.0.1", "debug": "^4.3.4", - "fs-extra": "^11.2.0" + "fs-extra": "^8.1.0" }, "engines": { "node": ">= 14" } }, - "node_modules/get-uri/node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/get-uri/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "dependencies": { "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" }, "engines": { - "node": ">=14.14" + "node": ">=6 <7 || >=8" + } + }, + "node_modules/get-uri/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/get-uri/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" } }, "node_modules/get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "dev": true, "engines": { "node": ">=0.10.0" @@ -5601,7 +5700,7 @@ "node_modules/glob-stream": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", "dev": true, "dependencies": { "extend": "^3.0.0", @@ -5623,7 +5722,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -5643,7 +5741,7 @@ "node_modules/glob-stream/node_modules/glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, "dependencies": { "is-glob": "^3.1.0", @@ -5653,7 +5751,7 @@ "node_modules/glob-stream/node_modules/is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "dependencies": { "is-extglob": "^2.1.0" @@ -5708,7 +5806,7 @@ "node_modules/glob-watcher/node_modules/anymatch/node_modules/normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, "dependencies": { "remove-trailing-separator": "^1.0.1" @@ -5762,23 +5860,11 @@ "node": ">=0.10.0" } }, - "node_modules/glob-watcher/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/glob-watcher/node_modules/chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", + "deprecated": "Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.", "dev": true, "dependencies": { "anymatch": "^2.0.0", @@ -5797,25 +5883,10 @@ "fsevents": "^1.2.7" } }, - "node_modules/glob-watcher/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/fill-range/node_modules/extend-shallow": { + "node_modules/glob-watcher/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "dependencies": { "is-extendable": "^0.1.0" @@ -5824,29 +5895,25 @@ "node": ">=0.10.0" } }, - "node_modules/glob-watcher/node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2", + "node_modules/glob-watcher/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "engines": { - "node": ">= 4.0" + "node": ">=0.10.0" } }, "node_modules/glob-watcher/node_modules/glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, "dependencies": { "is-glob": "^3.1.0", @@ -5856,7 +5923,7 @@ "node_modules/glob-watcher/node_modules/glob-parent/node_modules/is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "dependencies": { "is-extglob": "^2.1.0" @@ -5868,7 +5935,7 @@ "node_modules/glob-watcher/node_modules/is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", "dev": true, "dependencies": { "binary-extensions": "^1.0.0" @@ -5880,64 +5947,7 @@ "node_modules/glob-watcher/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/micromatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "dev": true, "engines": { "node": ">=0.10.0" @@ -5975,7 +5985,7 @@ "node_modules/glob-watcher/node_modules/to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, "dependencies": { "is-number": "^3.0.0", @@ -6026,7 +6036,7 @@ "node_modules/global-prefix": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", "dev": true, "dependencies": { "expand-tilde": "^2.0.2", @@ -6056,11 +6066,23 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6171,18 +6193,6 @@ "win32" ] }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/got": { "version": "12.6.1", "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", @@ -6221,9 +6231,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, "node_modules/grapheme-splitter": { @@ -6300,10 +6310,10 @@ "node": ">=0.10.0" } }, - "node_modules/gulp-cli/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "node_modules/gulp-cli/node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", "dev": true, "engines": { "node": ">=0.10.0" @@ -6312,7 +6322,7 @@ "node_modules/gulp-cli/node_modules/cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", "dev": true, "dependencies": { "string-width": "^1.0.1", @@ -6320,11 +6330,15 @@ "wrap-ansi": "^2.0.0" } }, - "node_modules/gulp-cli/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "node_modules/gulp-cli/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true, + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, "engines": { "node": ">=0.10.0" } @@ -6335,10 +6349,16 @@ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, + "node_modules/gulp-cli/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, "node_modules/gulp-cli/node_modules/is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "dependencies": { "number-is-nan": "^1.0.0" @@ -6347,10 +6367,70 @@ "node": ">=0.10.0" } }, + "node_modules/gulp-cli/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/gulp-cli/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/gulp-cli/node_modules/string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "dependencies": { "code-point-at": "^1.0.0", @@ -6364,7 +6444,7 @@ "node_modules/gulp-cli/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "dependencies": { "ansi-regex": "^2.0.0" @@ -6376,7 +6456,7 @@ "node_modules/gulp-cli/node_modules/wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "dependencies": { "string-width": "^1.0.1", @@ -6437,6 +6517,31 @@ "node": ">= 0.10" } }, + "node_modules/gulp-concat/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-concat/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/gulp-gzip": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/gulp-gzip/-/gulp-gzip-1.4.2.tgz", @@ -6466,6 +6571,31 @@ "node": ">=0.10.0" } }, + "node_modules/gulp-gzip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-gzip/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/gulp-header": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-2.0.9.tgz", @@ -6478,6 +6608,31 @@ "through2": "^2.0.0" } }, + "node_modules/gulp-header/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-header/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/gulp-insert": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/gulp-insert/-/gulp-insert-0.5.0.tgz", @@ -6617,6 +6772,21 @@ "node": ">=0.4.0" } }, + "node_modules/gulp-sourcemaps/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/gulp-sourcemaps/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6626,6 +6796,16 @@ "node": ">=0.10.0" } }, + "node_modules/gulp-sourcemaps/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/gulp-umd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/gulp-umd/-/gulp-umd-2.0.0.tgz", @@ -6637,10 +6817,35 @@ "through2": "^2.0.3" } }, + "node_modules/gulp-umd/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-umd/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/gulplog": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw==", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", "dev": true, "dependencies": { "glogg": "^1.0.0" @@ -6649,43 +6854,31 @@ "node": ">= 0.10" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "dependencies": { - "es-define-property": "^1.0.0" + "function-bind": "^1.1.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.4.0" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "dev": true, "engines": { "node": ">= 0.4" @@ -6697,7 +6890,7 @@ "node_modules/has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", "dev": true, "dependencies": { "get-value": "^2.0.6", @@ -6711,7 +6904,7 @@ "node_modules/has-values": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", "dev": true, "dependencies": { "is-number": "^3.0.0", @@ -6721,34 +6914,10 @@ "node": ">=0.10.0" } }, - "node_modules/has-values/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-values/node_modules/kind-of": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", "dev": true, "dependencies": { "is-buffer": "^1.1.5" @@ -6790,12 +6959,6 @@ "node": ">=0.10.0" } }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -6881,9 +7044,9 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -7016,29 +7179,16 @@ "node_modules/invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", "dev": true, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dev": true, - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" + "node": ">=0.10.0" } }, - "node_modules/ip-address/node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "node_modules/ip": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", "dev": true }, "node_modules/is-absolute": { @@ -7055,21 +7205,30 @@ } }, "node_modules/is-accessor-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", - "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "kind-of": "^6.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, "node_modules/is-binary-path": { @@ -7103,28 +7262,47 @@ } }, "node_modules/is-data-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", - "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "kind-of": "^6.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" + } + }, + "node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" + } + }, + "node_modules/is-descriptor/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/is-docker": { @@ -7199,17 +7377,32 @@ "node_modules/is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, "engines": { "node": ">=0.10.0" } @@ -7223,18 +7416,6 @@ "node": ">=8" } }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -7306,13 +7487,13 @@ "node_modules/is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", "dev": true }, "node_modules/is-valid-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", "dev": true, "engines": { "node": ">=0.10.0" @@ -7412,12 +7593,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true - }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", @@ -7702,7 +7877,7 @@ "node_modules/last-run": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ==", + "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", "dev": true, "dependencies": { "default-resolution": "^2.0.0", @@ -7742,7 +7917,7 @@ "node_modules/lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", "dev": true, "dependencies": { "invert-kv": "^1.0.0" @@ -7754,7 +7929,7 @@ "node_modules/lead": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", "dev": true, "dependencies": { "flush-write-stream": "^1.0.2" @@ -7819,7 +7994,7 @@ "node_modules/load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "dependencies": { "graceful-fs": "^4.1.2", @@ -7832,6 +8007,18 @@ "node": ">=0.10.0" } }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/locate-app": { "version": "2.4.21", "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.4.21.tgz", @@ -7853,18 +8040,6 @@ "userhome": "1.0.0" } }, - "node_modules/locate-app/node_modules/type-fest": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.13.0.tgz", - "integrity": "sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8042,7 +8217,7 @@ "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", "dev": true, "engines": { "node": ">=0.10.0" @@ -8057,7 +8232,7 @@ "node_modules/map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", "dev": true, "dependencies": { "object-visit": "^1.0.0" @@ -8078,7 +8253,7 @@ "node_modules/matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA==", + "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", "dev": true, "dependencies": { "findup-sync": "^2.0.0", @@ -8090,70 +8265,10 @@ "node": ">= 0.10.0" } }, - "node_modules/matchdep/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/matchdep/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/matchdep/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/matchdep/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/matchdep/node_modules/findup-sync": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", "dev": true, "dependencies": { "detect-file": "^1.0.0", @@ -8165,19 +8280,10 @@ "node": ">= 0.10" } }, - "node_modules/matchdep/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/matchdep/node_modules/is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "dependencies": { "is-extglob": "^2.1.0" @@ -8186,40 +8292,38 @@ "node": ">=0.10.0" } }, - "node_modules/matchdep/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "node_modules/memoizee": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", + "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", "dev": true, "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "d": "^1.0.1", + "es5-ext": "^0.10.53", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" } }, - "node_modules/matchdep/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } + "node_modules/memoizee/node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true }, - "node_modules/matchdep/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, - "node_modules/matchdep/node_modules/micromatch": { + "node_modules/micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", @@ -8243,55 +8347,95 @@ "node": ">=0.10.0" } }, - "node_modules/matchdep/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "node_modules/micromatch/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, "dependencies": { + "extend-shallow": "^2.0.1", "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/memoizee": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", + "node_modules/micromatch/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/micromatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "engines": { - "node": ">= 8" + "node": ">=0.10.0" } }, - "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "node_modules/micromatch/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" }, "engines": { - "node": ">=8.6" + "node": ">=0.10.0" } }, "node_modules/mime": { @@ -8581,13 +8725,6 @@ "node": ">= 0.10" } }, - "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", - "dev": true, - "optional": true - }, "node_modules/nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -8647,9 +8784,9 @@ } }, "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, "node_modules/nise": { @@ -8685,42 +8822,23 @@ } }, "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "whatwg-url": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "4.x || >=6.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/normalize-path": { @@ -8759,7 +8877,7 @@ "node_modules/number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true, "engines": { "node": ">=0.10.0" @@ -8782,7 +8900,7 @@ "node_modules/object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", "dev": true, "dependencies": { "copy-descriptor": "^0.1.0", @@ -8796,7 +8914,7 @@ "node_modules/object-copy/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "dependencies": { "is-descriptor": "^0.1.0" @@ -8805,23 +8923,57 @@ "node": ">=0.10.0" } }, + "node_modules/object-copy/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/object-copy/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "dependencies": { "is-buffer": "^1.1.5" @@ -8842,7 +8994,7 @@ "node_modules/object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", "dev": true, "dependencies": { "isobject": "^3.0.0" @@ -8852,14 +9004,14 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", "object-keys": "^1.1.1" }, "engines": { @@ -8872,7 +9024,7 @@ "node_modules/object.defaults": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", "dev": true, "dependencies": { "array-each": "^1.0.1", @@ -8887,7 +9039,7 @@ "node_modules/object.map": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", "dev": true, "dependencies": { "for-own": "^1.0.0", @@ -8900,7 +9052,7 @@ "node_modules/object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", "dev": true, "dependencies": { "isobject": "^3.0.1" @@ -8912,7 +9064,7 @@ "node_modules/object.reduce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", - "integrity": "sha512-naLhxxpUESbNkRqc35oQ2scZSJueHGQNUfMW/0U37IgN6tE2dgDWg3whf+NEliy3F/QysrO48XKUz/nGPe+AQw==", + "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", "dev": true, "dependencies": { "for-own": "^1.0.0", @@ -8976,7 +9128,7 @@ "node_modules/ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", "dev": true, "dependencies": { "readable-stream": "^2.0.1" @@ -9000,7 +9152,7 @@ "node_modules/os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "dependencies": { "lcid": "^1.0.0" @@ -9058,9 +9210,9 @@ } }, "node_modules/pac-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", - "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", + "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", "dev": true, "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", @@ -9068,21 +9220,22 @@ "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.5", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.4" + "https-proxy-agent": "^7.0.2", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.2" }, "engines": { "node": ">= 14" } }, "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.0.tgz", + "integrity": "sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg==", "dev": true, "dependencies": { "degenerator": "^5.0.0", + "ip": "^1.1.8", "netmask": "^2.0.2" }, "engines": { @@ -9110,7 +9263,7 @@ "node_modules/parse-filepath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", "dev": true, "dependencies": { "is-absolute": "^1.0.0", @@ -9134,18 +9287,6 @@ "node": ">= 18" } }, - "node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", - "dev": true, - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -9158,7 +9299,7 @@ "node_modules/parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", "dev": true, "engines": { "node": ">=0.10.0" @@ -9178,7 +9319,7 @@ "node_modules/pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", "dev": true, "engines": { "node": ">=0.10.0" @@ -9273,7 +9414,7 @@ "node_modules/path-dirname": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", "dev": true }, "node_modules/path-exists": { @@ -9312,7 +9453,7 @@ "node_modules/path-root": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", "dev": true, "dependencies": { "path-root-regex": "^0.1.0" @@ -9324,7 +9465,7 @@ "node_modules/path-root-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", "dev": true, "engines": { "node": ">=0.10.0" @@ -9373,7 +9514,7 @@ "node_modules/path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "dependencies": { "graceful-fs": "^4.1.2", @@ -9396,7 +9537,7 @@ "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", "dev": true }, "node_modules/picocolors": { @@ -9420,7 +9561,7 @@ "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true, "engines": { "node": ">=0.10.0" @@ -9429,7 +9570,7 @@ "node_modules/pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", "dev": true, "engines": { "node": ">=0.10.0" @@ -9438,7 +9579,7 @@ "node_modules/pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "dev": true, "dependencies": { "pinkie": "^2.0.0" @@ -9509,7 +9650,7 @@ "node_modules/posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true, "engines": { "node": ">=0.10.0" @@ -9568,7 +9709,7 @@ "node_modules/pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true, "engines": { "node": ">= 0.8" @@ -9599,19 +9740,19 @@ } }, "node_modules/proxy-agent": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", - "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", + "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", "dev": true, "dependencies": { "agent-base": "^7.0.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.0", "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.1", + "pac-proxy-agent": "^7.0.0", "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.2" + "socks-proxy-agent": "^8.0.1" }, "engines": { "node": ">= 14" @@ -9638,9 +9779,9 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, "node_modules/pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, "dependencies": { "end-of-stream": "^1.1.0", @@ -9658,6 +9799,16 @@ "pump": "^2.0.0" } }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9691,101 +9842,12 @@ } } }, - "node_modules/puppeteer-core/node_modules/@puppeteer/browsers": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.4.6.tgz", - "integrity": "sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==", - "dev": true, - "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "progress": "2.0.3", - "proxy-agent": "6.3.0", - "tar-fs": "3.0.4", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=16.3.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/puppeteer-core/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/puppeteer-core/node_modules/devtools-protocol": { "version": "0.0.1147663", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz", "integrity": "sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==", "dev": true }, - "node_modules/puppeteer-core/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/puppeteer-core/node_modules/proxy-agent": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", - "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", - "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/puppeteer-core/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/qs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", @@ -9829,80 +9891,28 @@ "node_modules/queue-tick": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", - "dev": true, - "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true }, - "node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, - "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", - "dev": true, - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" + "node": ">=10" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-pkg-up/node_modules/path-exists": { + "node_modules/randombytes": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "dependencies": { - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "safe-buffer": "^5.1.0" } }, "node_modules/readable-stream": { @@ -9973,7 +9983,7 @@ "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", "dev": true, "dependencies": { "resolve": "^1.1.6" @@ -10017,7 +10027,7 @@ "node_modules/remove-bom-stream": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", - "integrity": "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA==", + "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", "dev": true, "dependencies": { "remove-bom-buffer": "^3.0.0", @@ -10028,6 +10038,31 @@ "node": ">= 0.10" } }, + "node_modules/remove-bom-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/remove-bom-stream/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -10046,7 +10081,7 @@ "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true, "engines": { "node": ">=0.10" @@ -10055,7 +10090,7 @@ "node_modules/replace-homedir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", - "integrity": "sha512-CHPV/GAglbIB1tnQgaiysb8H2yCy8WQ7lcEwQ/eT+kLj0QHV8LnJW0zpqpE7RSkrMSRoa+EBoag86clf7WAgSg==", + "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", "dev": true, "dependencies": { "homedir-polyfill": "^1.0.1", @@ -10122,7 +10157,7 @@ "node_modules/require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, "node_modules/requires-port": { @@ -10156,7 +10191,7 @@ "node_modules/resolve-dir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", "dev": true, "dependencies": { "expand-tilde": "^2.0.0", @@ -10178,7 +10213,7 @@ "node_modules/resolve-options": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", - "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", + "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", "dev": true, "dependencies": { "value-or-function": "^3.0.0" @@ -10190,7 +10225,7 @@ "node_modules/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "deprecated": "https://github.com/lydell/resolve-url#deprecated", "dev": true }, @@ -10327,7 +10362,7 @@ "node_modules/safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "dependencies": { "ret": "~0.1.10" @@ -10373,7 +10408,7 @@ "node_modules/semver-greatest-satisfied-range": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", - "integrity": "sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ==", + "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", "dev": true, "dependencies": { "sver-compat": "^1.5.0" @@ -10383,9 +10418,9 @@ } }, "node_modules/serialize-error": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", - "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.2.tgz", + "integrity": "sha512-o43i0jLcA0LXA5Uu+gI1Vj+lF66KR9IAcy0ThbGq1bAMPN+k5IgSHsulfnqf/ddKAz6dWf+k8PD5hAr9oCSHEQ==", "dev": true, "dependencies": { "type-fest": "^2.12.2" @@ -10397,18 +10432,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -10421,26 +10444,9 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -10459,7 +10465,7 @@ "node_modules/set-value/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "dependencies": { "is-extendable": "^0.1.0" @@ -10471,7 +10477,7 @@ "node_modules/set-value/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "dev": true, "engines": { "node": ">=0.10.0" @@ -10625,7 +10631,7 @@ "node_modules/snapdragon-node/node_modules/define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "dependencies": { "is-descriptor": "^1.0.0" @@ -10649,7 +10655,7 @@ "node_modules/snapdragon-util/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "dependencies": { "is-buffer": "^1.1.5" @@ -10670,7 +10676,7 @@ "node_modules/snapdragon/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "dependencies": { "is-descriptor": "^0.1.0" @@ -10682,7 +10688,7 @@ "node_modules/snapdragon/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "dependencies": { "is-extendable": "^0.1.0" @@ -10691,23 +10697,72 @@ "node": ">=0.10.0" } }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, "node_modules/snapdragon/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "dev": true, "engines": { "node": ">=0.10.0" @@ -10716,14 +10771,13 @@ "node_modules/snapdragon/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, "node_modules/snapdragon/node_modules/source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", "dev": true, "dependencies": { "atob": "^2.1.2", @@ -10734,33 +10788,39 @@ } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", "dev": true, "dependencies": { - "ip-address": "^9.0.5", + "ip": "^2.0.0", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.0.0", + "node": ">= 10.13.0", "npm": ">= 3.0.0" } }, "node_modules/socks-proxy-agent": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", - "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", + "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", "dev": true, "dependencies": { - "agent-base": "^7.1.1", + "agent-base": "^7.0.2", "debug": "^4.3.4", - "socks": "^2.8.3" + "socks": "^2.7.1" }, "engines": { "node": ">= 14" } }, + "node_modules/socks/node_modules/ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "dev": true + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -10774,7 +10834,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", "dev": true, "dependencies": { "atob": "^2.1.2", @@ -10785,7 +10844,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "deprecated": "See https://github.com/lydell/source-map-url#deprecated", "dev": true }, "node_modules/spacetrim": { @@ -10820,9 +10878,9 @@ "dev": true }, "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -10881,7 +10939,7 @@ "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", "dev": true, "engines": { "node": "*" @@ -10890,7 +10948,7 @@ "node_modules/static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", "dev": true, "dependencies": { "define-property": "^0.2.5", @@ -10903,7 +10961,7 @@ "node_modules/static-extend/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "dependencies": { "is-descriptor": "^0.1.0" @@ -10912,17 +10970,66 @@ "node": ">=0.10.0" } }, + "node_modules/static-extend/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, "node_modules/stream-exhaust": { @@ -10932,9 +11039,9 @@ "dev": true }, "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", "dev": true }, "node_modules/stream-to-array": { @@ -11089,7 +11196,7 @@ "node_modules/strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, "dependencies": { "is-utf8": "^0.2.0" @@ -11146,7 +11253,7 @@ "node_modules/sver-compat": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", - "integrity": "sha512-aFTHfmjwizMNlNE6dsGmoAM4lHjL0CyiobWaFiXWSlD7cIxshW422Nb8KbXCmR6z+0ZEPY+daXJrDyh/vuwTyg==", + "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", "dev": true, "dependencies": { "es6-iterator": "^2.0.1", @@ -11191,20 +11298,10 @@ "tar-stream": "^3.1.5" } }, - "node_modules/tar-fs/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", + "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", "dev": true, "dependencies": { "b4a": "^1.6.4", @@ -11213,9 +11310,9 @@ } }, "node_modules/text-decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", - "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", "dev": true, "dependencies": { "b4a": "^1.6.4" @@ -11245,16 +11342,6 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/through2-filter": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", @@ -11265,7 +11352,7 @@ "xtend": "~4.0.0" } }, - "node_modules/through2/node_modules/readable-stream": { + "node_modules/through2-filter/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", @@ -11280,6 +11367,16 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/through2-filter/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/time-stamp": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", @@ -11314,7 +11411,7 @@ "node_modules/to-absolute-glob": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", "dev": true, "dependencies": { "is-absolute": "^1.0.0", @@ -11327,7 +11424,7 @@ "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", "dev": true, "dependencies": { "kind-of": "^3.0.2" @@ -11339,7 +11436,7 @@ "node_modules/to-object-path/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "dependencies": { "is-buffer": "^1.1.5" @@ -11387,7 +11484,7 @@ "node_modules/to-through": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", + "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", "dev": true, "dependencies": { "through2": "^2.0.3" @@ -11396,6 +11493,31 @@ "node": ">= 0.10" } }, + "node_modules/to-through/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/to-through/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -11479,12 +11601,12 @@ } }, "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.13.0.tgz", + "integrity": "sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -11522,7 +11644,7 @@ "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", "dev": true, "engines": { "node": ">=0.10.0" @@ -11552,7 +11674,7 @@ "node_modules/undertaker-registry": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw==", + "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", "dev": true, "engines": { "node": ">= 0.10" @@ -11561,13 +11683,13 @@ "node_modules/undertaker/node_modules/fast-levenshtein": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", - "integrity": "sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw==", + "integrity": "sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk=", "dev": true }, "node_modules/undici": { - "version": "5.28.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", - "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "version": "5.28.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", + "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", "dev": true, "dependencies": { "@fastify/busboy": "^2.0.0" @@ -11606,7 +11728,7 @@ "node_modules/union-value/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "dev": true, "engines": { "node": ">=0.10.0" @@ -11634,7 +11756,7 @@ "node_modules/unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", "dev": true, "dependencies": { "has-value": "^0.3.1", @@ -11647,7 +11769,7 @@ "node_modules/unset-value/node_modules/has-value": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", "dev": true, "dependencies": { "get-value": "^2.0.3", @@ -11661,7 +11783,7 @@ "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", "dev": true, "dependencies": { "isarray": "1.0.0" @@ -11673,7 +11795,7 @@ "node_modules/unset-value/node_modules/has-values": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", "dev": true, "engines": { "node": ">=0.10.0" @@ -11701,7 +11823,7 @@ "node_modules/urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "deprecated": "Please see https://github.com/lydell/urix#deprecated", "dev": true }, @@ -11778,7 +11900,7 @@ "node_modules/value-or-function": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", - "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", + "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", "dev": true, "engines": { "node": ">= 0.10" @@ -11844,10 +11966,20 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/vinyl-fs/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/vinyl-sourcemap": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", - "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", + "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", "dev": true, "dependencies": { "append-buffer": "^1.0.2", @@ -11865,7 +11997,7 @@ "node_modules/vinyl-sourcemap/node_modules/normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, "dependencies": { "remove-trailing-separator": "^1.0.1" @@ -11952,9 +12084,9 @@ } }, "node_modules/webdriverio": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.39.1.tgz", - "integrity": "sha512-dPwLgLNtP+l4vnybz+YFxxH8nBKOP7j6VVzKtfDyTLDQg9rz3U8OA4xMMQCBucnrVXy3KcKxGqlnMa+c4IfWCQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.39.0.tgz", + "integrity": "sha512-pDpGu0V+TL1LkXPode67m3s+IPto4TcmcOzMpzFgu2oeLMBornoLN3yQSFR1fjZd1gK4UfnG3lJ4poTGOfbWfw==", "dev": true, "dependencies": { "@types/node": "^20.1.0", @@ -12004,10 +12136,22 @@ "balanced-match": "^1.0.0" } }, + "node_modules/webdriverio/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/webdriverio/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -12081,7 +12225,7 @@ "node_modules/which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", "dev": true }, "node_modules/workerpool": { @@ -12290,22 +12434,13 @@ "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, - "node_modules/yauzl/node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", From 32f8e2433777bffd6a931f7177b768a90c10bcd8 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 18 Jul 2024 11:01:22 -0700 Subject: [PATCH 022/222] refactor: update the variable interfaces. (#8388) --- core/interfaces/i_variable_map.ts | 17 +++-------- core/interfaces/i_variable_model.ts | 33 +++++++++++++++++++- core/registry.ts | 14 +++++++++ core/serialization/variables.ts | 47 ++++++++--------------------- core/variable_model.ts | 35 +++++++++++++++++++-- 5 files changed, 97 insertions(+), 49 deletions(-) diff --git a/core/interfaces/i_variable_map.ts b/core/interfaces/i_variable_map.ts index 0bfc532a76e..6c21aa8e0cb 100644 --- a/core/interfaces/i_variable_map.ts +++ b/core/interfaces/i_variable_map.ts @@ -4,8 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {IVariableModel} from './i_variable_model.js'; -import {State} from '../serialization/variables.js'; +import type {IVariableModel, IVariableState} from './i_variable_model.js'; /** * Variable maps are container objects responsible for storing and managing the @@ -14,7 +13,7 @@ import {State} from '../serialization/variables.js'; * Any of these methods may define invariants about which names and types are * legal, and throw if they are not met. */ -export interface IVariableMap { +export interface IVariableMap> { /* Returns the variable corresponding to the given ID, or null if none. */ getVariableById(id: string): T | null; @@ -46,6 +45,9 @@ export interface IVariableMap { */ createVariable(name: string, id?: string, type?: string | null): T; + /* Adds a variable to this variable map. */ + addVariable(variable: T): void; + /** * Changes the name of the given variable to the name provided and returns the * renamed variable. @@ -60,13 +62,4 @@ export interface IVariableMap { /* Removes all variables from this variable map. */ clear(): void; - - /* Returns an object representing the serialized state of the variable. */ - saveVariable(variable: T): U; - - /** - * Creates a variable in this variable map corresponding to the given state - * (produced by a call to `saveVariable`). - */ - loadVariable(state: U): T; } diff --git a/core/interfaces/i_variable_model.ts b/core/interfaces/i_variable_model.ts index 97fa9161d41..791b1072567 100644 --- a/core/interfaces/i_variable_model.ts +++ b/core/interfaces/i_variable_model.ts @@ -4,8 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type {Workspace} from '../workspace.js'; + /* Representation of a variable. */ -export interface IVariableModel { +export interface IVariableModel { /* Returns the unique ID of this variable. */ getId(): string; @@ -23,4 +25,33 @@ export interface IVariableModel { /* Sets the type of this variable. */ setType(type: string): this; + + getWorkspace(): Workspace; + + /* Serializes this variable */ + save(): T; +} + +export interface IVariableModelStatic { + new ( + workspace: Workspace, + name: string, + type?: string, + id?: string, + ): IVariableModel; + + /** + * Creates a new IVariableModel corresponding to the given state on the + * specified workspace. This method must be static in your implementation. + */ + load(state: T, workspace: Workspace): IVariableModel; +} + +/** + * Represents the state of a given variable. + */ +export interface IVariableState { + name: string; + id: string; + type?: string; } diff --git a/core/registry.ts b/core/registry.ts index d46c36f4819..c7e16e935e7 100644 --- a/core/registry.ts +++ b/core/registry.ts @@ -24,6 +24,12 @@ import type {IPaster} from './interfaces/i_paster.js'; import type {ICopyData, ICopyable} from './interfaces/i_copyable.js'; import type {IConnectionPreviewer} from './interfaces/i_connection_previewer.js'; import type {IDragger} from './interfaces/i_dragger.js'; +import type { + IVariableModel, + IVariableModelStatic, + IVariableState, +} from './interfaces/i_variable_model.js'; +import type {IVariableMap} from './interfaces/i_variable_map.js'; /** * A map of maps. With the keys being the type and name of the class we are @@ -109,6 +115,14 @@ export class Type<_T> { /** @internal */ static PASTER = new Type>>('paster'); + + static VARIABLE_MODEL = new Type>( + 'variableModel', + ); + + static VARIABLE_MAP = new Type>>( + 'variableMap', + ); } /** diff --git a/core/serialization/variables.ts b/core/serialization/variables.ts index 69c6cda8c1b..62a52c41f7a 100644 --- a/core/serialization/variables.ts +++ b/core/serialization/variables.ts @@ -7,20 +7,13 @@ // Former goog.module ID: Blockly.serialization.variables import type {ISerializer} from '../interfaces/i_serializer.js'; +import type {IVariableState} from '../interfaces/i_variable_model.js'; import type {Workspace} from '../workspace.js'; import * as priorities from './priorities.js'; +import * as registry from '../registry.js'; import * as serializationRegistry from './registry.js'; -/** - * Represents the state of a given variable. - */ -export interface State { - name: string; - id: string; - type: string | undefined; -} - /** * Serializer for saving and loading variable state. */ @@ -40,23 +33,9 @@ export class VariableSerializer implements ISerializer { * @returns The state of the workspace's variables, or null if there are no * variables. */ - save(workspace: Workspace): State[] | null { - const variableStates = []; - for (const variable of workspace.getAllVariables()) { - const state = { - 'name': variable.name, - 'id': variable.getId(), - }; - if (variable.type) { - (state as AnyDuringMigration)['type'] = variable.type; - } - variableStates.push(state); - } - // AnyDuringMigration because: Type '{ name: string; id: string; }[] | - // null' is not assignable to type 'State[] | null'. - return ( - variableStates.length ? variableStates : null - ) as AnyDuringMigration; + save(workspace: Workspace): IVariableState[] | null { + const variableStates = workspace.getAllVariables().map((v) => v.save()); + return variableStates.length ? variableStates : null; } /** @@ -66,14 +45,14 @@ export class VariableSerializer implements ISerializer { * @param state The state of the variables to deserialize. * @param workspace The workspace to deserialize into. */ - load(state: State[], workspace: Workspace) { - for (const varState of state) { - workspace.createVariable( - varState['name'], - varState['type'], - varState['id'], - ); - } + load(state: IVariableState[], workspace: Workspace) { + const VariableModel = registry.getObject( + registry.Type.VARIABLE_MODEL, + registry.DEFAULT, + ); + state.forEach((s) => { + VariableModel?.load(s, workspace); + }); } /** diff --git a/core/variable_model.ts b/core/variable_model.ts index c4ec05367e2..017b02c6093 100644 --- a/core/variable_model.ts +++ b/core/variable_model.ts @@ -15,8 +15,9 @@ import './events/events_var_create.js'; import * as idGenerator from './utils/idgenerator.js'; +import * as registry from './registry.js'; import type {Workspace} from './workspace.js'; -import {IVariableModel} from './interfaces/i_variable_model.js'; +import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; /** * Class for a variable model. @@ -24,7 +25,7 @@ import {IVariableModel} from './interfaces/i_variable_model.js'; * * @see {Blockly.FieldVariable} */ -export class VariableModel implements IVariableModel { +export class VariableModel implements IVariableModel { type: string; private readonly id_: string; @@ -95,6 +96,30 @@ export class VariableModel implements IVariableModel { return this; } + getWorkspace(): Workspace { + return this.workspace; + } + + save(): IVariableState { + const state: IVariableState = { + 'name': this.getName(), + 'id': this.getId(), + }; + const type = this.getType(); + if (type) { + state['type'] = type; + } + + return state; + } + + static load(state: IVariableState, workspace: Workspace) { + // TODO(adodson): Once VariableMap implements IVariableMap, directly + // construct a variable, retrieve the variable map from the workspace, + // add the variable to that variable map, and fire a VAR_CREATE event. + workspace.createVariable(state['name'], state['type'], state['id']); + } + /** * A custom compare function for the VariableModel objects. * @@ -108,3 +133,9 @@ export class VariableModel implements IVariableModel { return var1.name.localeCompare(var2.name, undefined, {sensitivity: 'base'}); } } + +registry.register( + registry.Type.VARIABLE_MODEL, + registry.DEFAULT, + VariableModel, +); From 02e64bebbe986fd05cd753fefdefd853a66ca16f Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 19 Jul 2024 10:53:16 -0700 Subject: [PATCH 023/222] refactor: make VariableMap implement IVariableMap. (#8395) * refactor: make VariableMap implement IVariableMap. * chore: remove unused arrayUtils import. * chore: fix comment on variable map backing store. * chore: Added JSDoc to new VariableMap methods. * chore: Improve test descriptions. --- core/variable_map.ts | 148 ++++++++++++++++++------------- core/variables.ts | 6 +- core/workspace.ts | 8 +- tests/mocha/variable_map_test.js | 73 +++++++++++++-- 4 files changed, 164 insertions(+), 71 deletions(-) diff --git a/core/variable_map.ts b/core/variable_map.ts index bc19b07a59a..b8e2e4e0af6 100644 --- a/core/variable_map.ts +++ b/core/variable_map.ts @@ -19,25 +19,26 @@ import './events/events_var_rename.js'; import type {Block} from './block.js'; import * as dialog from './dialog.js'; import * as eventUtils from './events/utils.js'; +import * as registry from './registry.js'; import {Msg} from './msg.js'; import {Names} from './names.js'; -import * as arrayUtils from './utils/array.js'; import * as idGenerator from './utils/idgenerator.js'; import {VariableModel} from './variable_model.js'; import type {Workspace} from './workspace.js'; +import type {IVariableMap} from './interfaces/i_variable_map.js'; /** * Class for a variable map. This contains a dictionary data structure with * variable types as keys and lists of variables as values. The list of * variables are the type indicated by the key. */ -export class VariableMap { +export class VariableMap implements IVariableMap { /** - * A map from variable type to list of variable names. The lists contain + * A map from variable type to map of IDs to variables. The maps contain * all of the named variables in the workspace, including variables that are * not currently in use. */ - private variableMap = new Map(); + private variableMap = new Map>(); /** @param workspace The workspace this map belongs to. */ constructor(public workspace: Workspace) {} @@ -45,8 +46,8 @@ export class VariableMap { /** Clear the variable map. Fires events for every deletion. */ clear() { for (const variables of this.variableMap.values()) { - while (variables.length > 0) { - this.deleteVariable(variables[0]); + for (const variable of variables.values()) { + this.deleteVariable(variable); } } if (this.variableMap.size !== 0) { @@ -60,10 +61,10 @@ export class VariableMap { * * @param variable Variable to rename. * @param newName New variable name. - * @internal + * @returns The newly renamed variable. */ - renameVariable(variable: VariableModel, newName: string) { - if (variable.name === newName) return; + renameVariable(variable: VariableModel, newName: string): VariableModel { + if (variable.name === newName) return variable; const type = variable.type; const conflictVar = this.getVariable(newName, type); const blocks = this.workspace.getAllBlocks(false); @@ -87,6 +88,20 @@ export class VariableMap { } finally { eventUtils.setGroup(existingGroup); } + return variable; + } + + changeVariableType(variable: VariableModel, newType: string): VariableModel { + this.variableMap.get(variable.getType())?.delete(variable.getId()); + variable.setType(newType); + const newTypeVariables = + this.variableMap.get(newType) ?? new Map(); + newTypeVariables.set(variable.getId(), variable); + if (!this.variableMap.has(newType)) { + this.variableMap.set(newType, newTypeVariables); + } + + return variable; } /** @@ -159,8 +174,8 @@ export class VariableMap { } // Finally delete the original variable, which is now unreferenced. eventUtils.fire(new (eventUtils.get(eventUtils.VAR_DELETE))(variable)); - // And remove it from the list. - arrayUtils.removeElem(this.variableMap.get(type)!, variable); + // And remove it from the map. + this.variableMap.get(type)?.delete(variable.getId()); } /* End functions for renaming variables. */ @@ -177,8 +192,8 @@ export class VariableMap { */ createVariable( name: string, - opt_type?: string | null, - opt_id?: string | null, + opt_type?: string, + opt_id?: string, ): VariableModel { let variable = this.getVariable(name, opt_type); if (variable) { @@ -204,20 +219,30 @@ export class VariableMap { const type = opt_type || ''; variable = new VariableModel(this.workspace, name, type, id); - const variables = this.variableMap.get(type) || []; - variables.push(variable); - // Delete the list of variables of this type, and re-add it so that - // the most recent addition is at the end. - // This is used so the toolbox's set block is set to the most recent - // variable. - this.variableMap.delete(type); - this.variableMap.set(type, variables); - + const variables = + this.variableMap.get(type) ?? new Map(); + variables.set(variable.getId(), variable); + if (!this.variableMap.has(type)) { + this.variableMap.set(type, variables); + } eventUtils.fire(new (eventUtils.get(eventUtils.VAR_CREATE))(variable)); return variable; } + /** + * Adds the given variable to this variable map. + * + * @param variable The variable to add. + */ + addVariable(variable: VariableModel) { + const type = variable.getType(); + if (!this.variableMap.has(type)) { + this.variableMap.set(type, new Map()); + } + this.variableMap.get(type)?.set(variable.getId(), variable); + } + /* Begin functions for variable deletion. */ /** * Delete a variable. @@ -225,22 +250,12 @@ export class VariableMap { * @param variable Variable to delete. */ deleteVariable(variable: VariableModel) { - const variableId = variable.getId(); - const variableList = this.variableMap.get(variable.type); - if (variableList) { - for (let i = 0; i < variableList.length; i++) { - const tempVar = variableList[i]; - if (tempVar.getId() === variableId) { - variableList.splice(i, 1); - eventUtils.fire( - new (eventUtils.get(eventUtils.VAR_DELETE))(variable), - ); - if (variableList.length === 0) { - this.variableMap.delete(variable.type); - } - return; - } - } + const variables = this.variableMap.get(variable.type); + if (!variables || !variables.has(variable.getId())) return; + variables.delete(variable.getId()); + eventUtils.fire(new (eventUtils.get(eventUtils.VAR_DELETE))(variable)); + if (variables.size === 0) { + this.variableMap.delete(variable.type); } } @@ -321,17 +336,16 @@ export class VariableMap { * the empty string, which is a specific type. * @returns The variable with the given name, or null if it was not found. */ - getVariable(name: string, opt_type?: string | null): VariableModel | null { + getVariable(name: string, opt_type?: string): VariableModel | null { const type = opt_type || ''; - const list = this.variableMap.get(type); - if (list) { - for (let j = 0, variable; (variable = list[j]); j++) { - if (Names.equals(variable.name, name)) { - return variable; - } - } - } - return null; + const variables = this.variableMap.get(type); + if (!variables) return null; + + return ( + [...variables.values()].find((variable) => + Names.equals(variable.getName(), name), + ) ?? null + ); } /** @@ -342,10 +356,8 @@ export class VariableMap { */ getVariableById(id: string): VariableModel | null { for (const variables of this.variableMap.values()) { - for (const variable of variables) { - if (variable.getId() === id) { - return variable; - } + if (variables.has(id)) { + return variables.get(id) ?? null; } } return null; @@ -361,11 +373,19 @@ export class VariableMap { */ getVariablesOfType(type: string | null): VariableModel[] { type = type || ''; - const variableList = this.variableMap.get(type); - if (variableList) { - return variableList.slice(); - } - return []; + const variables = this.variableMap.get(type); + if (!variables) return []; + + return [...variables.values()]; + } + + /** + * Returns a list of unique types of variables in this variable map. + * + * @returns A list of unique types of variables in this variable map. + */ + getTypes(): string[] { + return [...this.variableMap.keys()]; } /** @@ -399,7 +419,7 @@ export class VariableMap { getAllVariables(): VariableModel[] { let allVariables: VariableModel[] = []; for (const variables of this.variableMap.values()) { - allVariables = allVariables.concat(variables); + allVariables = allVariables.concat(...variables.values()); } return allVariables; } @@ -410,9 +430,13 @@ export class VariableMap { * @returns All of the variable names of all types. */ getAllVariableNames(): string[] { - return Array.from(this.variableMap.values()) - .flat() - .map((variable) => variable.name); + const names: string[] = []; + for (const variables of this.variableMap.values()) { + for (const variable of variables.values()) { + names.push(variable.getName()); + } + } + return names; } /** @@ -438,3 +462,5 @@ export class VariableMap { return uses; } } + +registry.register(registry.Type.VARIABLE_MAP, registry.DEFAULT, VariableMap); diff --git a/core/variables.ts b/core/variables.ts index dee53a72bc8..da9d28bffc7 100644 --- a/core/variables.ts +++ b/core/variables.ts @@ -610,7 +610,11 @@ function createVariable( // Create a potential variable if in the flyout. let variable = null; if (potentialVariableMap) { - variable = potentialVariableMap.createVariable(opt_name, opt_type, id); + variable = potentialVariableMap.createVariable( + opt_name, + opt_type, + id ?? undefined, + ); } else { // In the main workspace, create a real variable. variable = workspace.createVariable(opt_name, opt_type, id); diff --git a/core/workspace.ts b/core/workspace.ts index 16f32611b2b..71a2e4af9f6 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -400,7 +400,11 @@ export class Workspace implements IASTNodeLocation { opt_type?: string | null, opt_id?: string | null, ): VariableModel { - return this.variableMap.createVariable(name, opt_type, opt_id); + return this.variableMap.createVariable( + name, + opt_type ?? undefined, + opt_id ?? undefined, + ); } /** @@ -456,7 +460,7 @@ export class Workspace implements IASTNodeLocation { * if none are found. */ getVariablesOfType(type: string | null): VariableModel[] { - return this.variableMap.getVariablesOfType(type); + return this.variableMap.getVariablesOfType(type ?? ''); } /** diff --git a/tests/mocha/variable_map_test.js b/tests/mocha/variable_map_test.js index c3d75e8a521..13a474245df 100644 --- a/tests/mocha/variable_map_test.js +++ b/tests/mocha/variable_map_test.js @@ -39,17 +39,17 @@ suite('Variable Map', function () { this.variableMap.createVariable('name1', 'type1', 'id1'); // Assert there is only one variable in the this.variableMap. - let keys = Array.from(this.variableMap.variableMap.keys()); + let keys = this.variableMap.getTypes(); assert.equal(keys.length, 1); - let varMapLength = this.variableMap.variableMap.get(keys[0]).length; + let varMapLength = this.variableMap.getVariablesOfType(keys[0]).length; assert.equal(varMapLength, 1); this.variableMap.createVariable('name1', 'type1'); assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); // Check that the size of the variableMap did not change. - keys = Array.from(this.variableMap.variableMap.keys()); + keys = this.variableMap.getTypes(); assert.equal(keys.length, 1); - varMapLength = this.variableMap.variableMap.get(keys[0]).length; + varMapLength = this.variableMap.getVariablesOfType(keys[0]).length; assert.equal(varMapLength, 1); }); @@ -59,16 +59,16 @@ suite('Variable Map', function () { this.variableMap.createVariable('name1', 'type1', 'id1'); // Assert there is only one variable in the this.variableMap. - let keys = Array.from(this.variableMap.variableMap.keys()); + let keys = this.variableMap.getTypes(); assert.equal(keys.length, 1); - const varMapLength = this.variableMap.variableMap.get(keys[0]).length; + const varMapLength = this.variableMap.getVariablesOfType(keys[0]).length; assert.equal(varMapLength, 1); this.variableMap.createVariable('name1', 'type2', 'id2'); assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); assertVariableValues(this.variableMap, 'name1', 'type2', 'id2'); // Check that the size of the variableMap did change. - keys = Array.from(this.variableMap.variableMap.keys()); + keys = this.variableMap.getTypes(); assert.equal(keys.length, 2); }); @@ -246,6 +246,65 @@ suite('Variable Map', function () { }); }); + suite( + 'Using changeVariableType to change the type of a variable', + function () { + test('updates it to a new non-empty value', function () { + const variable = this.variableMap.createVariable( + 'name1', + 'type1', + 'id1', + ); + this.variableMap.changeVariableType(variable, 'type2'); + const oldTypeVariables = this.variableMap.getVariablesOfType('type1'); + const newTypeVariables = this.variableMap.getVariablesOfType('type2'); + assert.deepEqual(oldTypeVariables, []); + assert.deepEqual(newTypeVariables, [variable]); + assert.equal(variable.getType(), 'type2'); + }); + + test('updates it to a new empty value', function () { + const variable = this.variableMap.createVariable( + 'name1', + 'type1', + 'id1', + ); + this.variableMap.changeVariableType(variable, ''); + const oldTypeVariables = this.variableMap.getVariablesOfType('type1'); + const newTypeVariables = this.variableMap.getVariablesOfType(''); + assert.deepEqual(oldTypeVariables, []); + assert.deepEqual(newTypeVariables, [variable]); + assert.equal(variable.getType(), ''); + }); + }, + ); + + suite('addVariable', function () { + test('normally', function () { + const variable = new Blockly.VariableModel(this.workspace, 'foo', 'int'); + assert.isNull(this.variableMap.getVariableById(variable.getId())); + this.variableMap.addVariable(variable); + assert.equal( + this.variableMap.getVariableById(variable.getId()), + variable, + ); + }); + }); + + suite('getTypes', function () { + test('when map is empty', function () { + const types = this.variableMap.getTypes(); + assert.deepEqual(types, []); + }); + + test('with various types', function () { + this.variableMap.createVariable('name1', 'type1', 'id1'); + this.variableMap.createVariable('name2', '', 'id2'); + const types = this.variableMap.getTypes(); + assert.deepEqual(types, ['type1', '']); + }); + }); + suite('getAllVariables', function () { test('Trivial', function () { const var1 = this.variableMap.createVariable('name1', 'type1', 'id1'); From 294ef74d1bac10873297161c66e37e96f19d43ff Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 19 Jul 2024 14:58:04 -0700 Subject: [PATCH 024/222] refactor: Use IVariableModel instead of VariableModel. (#8400) * refactor: Use IVariableModel methods instead of directly accessing properties. * refactor: replace references to VariableModel with IVariableModel. --- blocks/loops.ts | 2 +- blocks/procedures.ts | 47 ++++++---- blocks/variables_dynamic.ts | 4 +- core/block.ts | 9 +- core/events/events_var_base.ts | 9 +- core/events/events_var_create.ts | 11 ++- core/events/events_var_delete.ts | 11 ++- core/events/events_var_rename.ts | 9 +- core/field_variable.ts | 44 +++++----- .../i_variable_backed_parameter_model.ts | 4 +- core/names.ts | 2 +- core/serialization/blocks.ts | 7 +- core/variable_map.ts | 86 +++++++++++++------ core/variable_model.ts | 10 ++- core/variables.ts | 81 +++++++++++------ core/variables_dynamic.ts | 3 +- core/workspace.ts | 18 ++-- core/workspace_svg.ts | 7 +- core/xml.ts | 15 ++-- generators/php/procedures.ts | 2 +- generators/python/procedures.ts | 2 +- tests/mocha/xml_test.js | 6 ++ 22 files changed, 248 insertions(+), 141 deletions(-) diff --git a/blocks/loops.ts b/blocks/loops.ts index c7cb710d770..5835ab8bece 100644 --- a/blocks/loops.ts +++ b/blocks/loops.ts @@ -269,7 +269,7 @@ const CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN = { } const varField = this.getField('VAR') as FieldVariable; const variable = varField.getVariable()!; - const varName = variable.name; + const varName = variable.getName(); if (!this.isCollapsed() && varName !== null) { const getVarBlockState = { type: 'variables_get', diff --git a/blocks/procedures.ts b/blocks/procedures.ts index 1214eb55eda..583ca9d2087 100644 --- a/blocks/procedures.ts +++ b/blocks/procedures.ts @@ -32,7 +32,10 @@ import {FieldTextInput} from '../core/field_textinput.js'; import {Msg} from '../core/msg.js'; import {MutatorIcon as Mutator} from '../core/icons/mutator_icon.js'; import {Names} from '../core/names.js'; -import type {VariableModel} from '../core/variable_model.js'; +import type { + IVariableModel, + IVariableState, +} from '../core/interfaces/i_variable_model.js'; import type {Workspace} from '../core/workspace.js'; import type {WorkspaceSvg} from '../core/workspace_svg.js'; import {config} from '../core/config.js'; @@ -48,7 +51,7 @@ export const blocks: {[key: string]: BlockDefinition} = {}; type ProcedureBlock = Block & ProcedureMixin; interface ProcedureMixin extends ProcedureMixinType { arguments_: string[]; - argumentVarModels_: VariableModel[]; + argumentVarModels_: IVariableModel[]; callType_: string; paramIds_: string[]; hasStatements_: boolean; @@ -128,7 +131,7 @@ const PROCEDURE_DEF_COMMON = { for (let i = 0; i < this.argumentVarModels_.length; i++) { const parameter = xmlUtils.createElement('arg'); const argModel = this.argumentVarModels_[i]; - parameter.setAttribute('name', argModel.name); + parameter.setAttribute('name', argModel.getName()); parameter.setAttribute('varid', argModel.getId()); if (opt_paramIds && this.paramIds_) { parameter.setAttribute('paramId', this.paramIds_[i]); @@ -196,7 +199,7 @@ const PROCEDURE_DEF_COMMON = { state['params'].push({ // We don't need to serialize the name, but just in case we decide // to separate params from variables. - 'name': this.argumentVarModels_[i].name, + 'name': this.argumentVarModels_[i].getName(), 'id': this.argumentVarModels_[i].getId(), }); } @@ -224,7 +227,7 @@ const PROCEDURE_DEF_COMMON = { param['name'], '', ); - this.arguments_.push(variable.name); + this.arguments_.push(variable.getName()); this.argumentVarModels_.push(variable); } } @@ -352,7 +355,9 @@ const PROCEDURE_DEF_COMMON = { * * @returns List of variable models. */ - getVarModels: function (this: ProcedureBlock): VariableModel[] { + getVarModels: function ( + this: ProcedureBlock, + ): IVariableModel[] { return this.argumentVarModels_; }, /** @@ -370,23 +375,23 @@ const PROCEDURE_DEF_COMMON = { newId: string, ) { const oldVariable = this.workspace.getVariableById(oldId)!; - if (oldVariable.type !== '') { + if (oldVariable.getType() !== '') { // Procedure arguments always have the empty type. return; } - const oldName = oldVariable.name; + const oldName = oldVariable.getName(); const newVar = this.workspace.getVariableById(newId)!; let change = false; for (let i = 0; i < this.argumentVarModels_.length; i++) { if (this.argumentVarModels_[i].getId() === oldId) { - this.arguments_[i] = newVar.name; + this.arguments_[i] = newVar.getName(); this.argumentVarModels_[i] = newVar; change = true; } } if (change) { - this.displayRenamedVar_(oldName, newVar.name); + this.displayRenamedVar_(oldName, newVar.getName()); Procedures.mutateCallers(this); } }, @@ -398,9 +403,9 @@ const PROCEDURE_DEF_COMMON = { */ updateVarName: function ( this: ProcedureBlock & BlockSvg, - variable: VariableModel, + variable: IVariableModel, ) { - const newName = variable.name; + const newName = variable.getName(); let change = false; let oldName; for (let i = 0; i < this.argumentVarModels_.length; i++) { @@ -473,12 +478,16 @@ const PROCEDURE_DEF_COMMON = { const getVarBlockState = { type: 'variables_get', fields: { - VAR: {name: argVar.name, id: argVar.getId(), type: argVar.type}, + VAR: { + name: argVar.getName(), + id: argVar.getId(), + type: argVar.getType(), + }, }, }; options.push({ enabled: true, - text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', argVar.name), + text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', argVar.getName()), callback: ContextMenu.callbackFactory(this, getVarBlockState), }); } @@ -623,7 +632,7 @@ type ArgumentMixinType = typeof PROCEDURES_MUTATORARGUMENT; // TODO(#6920): This is kludgy. type FieldTextInputForArgument = FieldTextInput & { oldShowEditorFn_(_e?: Event, quietInput?: boolean): void; - createdVariables_: VariableModel[]; + createdVariables_: IVariableModel[]; }; const PROCEDURES_MUTATORARGUMENT = { @@ -708,7 +717,7 @@ const PROCEDURES_MUTATORARGUMENT = { } let model = outerWs.getVariable(varName, ''); - if (model && model.name !== varName) { + if (model && model.getName() !== varName) { // Rename the variable (case change) outerWs.renameVariableById(model.getId(), varName); } @@ -739,7 +748,7 @@ const PROCEDURES_MUTATORARGUMENT = { } for (let i = 0; i < this.createdVariables_.length; i++) { const model = this.createdVariables_[i]; - if (model.name !== newText) { + if (model.getName() !== newText) { outerWs.deleteVariableById(model.getId()); } } @@ -750,7 +759,7 @@ blocks['procedures_mutatorarg'] = PROCEDURES_MUTATORARGUMENT; /** Type of a block using the PROCEDURE_CALL_COMMON mixin. */ type CallBlock = Block & CallMixin; interface CallMixin extends CallMixinType { - argumentVarModels_: VariableModel[]; + argumentVarModels_: IVariableModel[]; arguments_: string[]; defType_: string; quarkIds_: string[] | null; @@ -1029,7 +1038,7 @@ const PROCEDURE_CALL_COMMON = { * * @returns List of variable models. */ - getVarModels: function (this: CallBlock): VariableModel[] { + getVarModels: function (this: CallBlock): IVariableModel[] { return this.argumentVarModels_; }, /** diff --git a/blocks/variables_dynamic.ts b/blocks/variables_dynamic.ts index e74cae423ab..ff94d8c96c4 100644 --- a/blocks/variables_dynamic.ts +++ b/blocks/variables_dynamic.ts @@ -144,9 +144,9 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = { const id = this.getFieldValue('VAR'); const variableModel = Variables.getVariable(this.workspace, id)!; if (this.type === 'variables_get_dynamic') { - this.outputConnection!.setCheck(variableModel.type); + this.outputConnection!.setCheck(variableModel.getType()); } else { - this.getInput('VALUE')!.connection!.setCheck(variableModel.type); + this.getInput('VALUE')!.connection!.setCheck(variableModel.getType()); } }, }; diff --git a/core/block.ts b/core/block.ts index 52191d63c3c..c08b75a694e 100644 --- a/core/block.ts +++ b/core/block.ts @@ -45,7 +45,10 @@ import * as idGenerator from './utils/idgenerator.js'; import * as parsing from './utils/parsing.js'; import * as registry from './registry.js'; import {Size} from './utils/size.js'; -import type {VariableModel} from './variable_model.js'; +import type { + IVariableModel, + IVariableState, +} from './interfaces/i_variable_model.js'; import type {Workspace} from './workspace.js'; import {DummyInput} from './inputs/dummy_input.js'; import {EndRowInput} from './inputs/end_row_input.js'; @@ -1133,7 +1136,7 @@ export class Block implements IASTNodeLocation { * @returns List of variable models. * @internal */ - getVarModels(): VariableModel[] { + getVarModels(): IVariableModel[] { const vars = []; for (let i = 0, input; (input = this.inputList[i]); i++) { for (let j = 0, field; (field = input.fieldRow[j]); j++) { @@ -1159,7 +1162,7 @@ export class Block implements IASTNodeLocation { * @param variable The variable being renamed. * @internal */ - updateVarName(variable: VariableModel) { + updateVarName(variable: IVariableModel) { for (let i = 0, input; (input = this.inputList[i]); i++) { for (let j = 0, field; (field = input.fieldRow[j]); j++) { if ( diff --git a/core/events/events_var_base.ts b/core/events/events_var_base.ts index 74537f144ff..1ec59ac6c40 100644 --- a/core/events/events_var_base.ts +++ b/core/events/events_var_base.ts @@ -11,7 +11,10 @@ */ // Former goog.module ID: Blockly.Events.VarBase -import type {VariableModel} from '../variable_model.js'; +import type { + IVariableModel, + IVariableState, +} from '../interfaces/i_variable_model.js'; import { Abstract as AbstractEvent, @@ -31,13 +34,13 @@ export class VarBase extends AbstractEvent { * @param opt_variable The variable this event corresponds to. Undefined for * a blank event. */ - constructor(opt_variable?: VariableModel) { + constructor(opt_variable?: IVariableModel) { super(); this.isBlank = typeof opt_variable === 'undefined'; if (!opt_variable) return; this.varId = opt_variable.getId(); - this.workspaceId = opt_variable.workspace.id; + this.workspaceId = opt_variable.getWorkspace().id; } /** diff --git a/core/events/events_var_create.ts b/core/events/events_var_create.ts index a719cad985a..0685b8ad831 100644 --- a/core/events/events_var_create.ts +++ b/core/events/events_var_create.ts @@ -12,7 +12,10 @@ // Former goog.module ID: Blockly.Events.VarCreate import * as registry from '../registry.js'; -import type {VariableModel} from '../variable_model.js'; +import type { + IVariableModel, + IVariableState, +} from '../interfaces/i_variable_model.js'; import {VarBase, VarBaseJson} from './events_var_base.js'; import * as eventUtils from './utils.js'; @@ -33,14 +36,14 @@ export class VarCreate extends VarBase { /** * @param opt_variable The created variable. Undefined for a blank event. */ - constructor(opt_variable?: VariableModel) { + constructor(opt_variable?: IVariableModel) { super(opt_variable); if (!opt_variable) { return; // Blank event to be populated by fromJson. } - this.varType = opt_variable.type; - this.varName = opt_variable.name; + this.varType = opt_variable.getType(); + this.varName = opt_variable.getName(); } /** diff --git a/core/events/events_var_delete.ts b/core/events/events_var_delete.ts index fc19461d4cd..d469db8069e 100644 --- a/core/events/events_var_delete.ts +++ b/core/events/events_var_delete.ts @@ -7,7 +7,10 @@ // Former goog.module ID: Blockly.Events.VarDelete import * as registry from '../registry.js'; -import type {VariableModel} from '../variable_model.js'; +import type { + IVariableModel, + IVariableState, +} from '../interfaces/i_variable_model.js'; import {VarBase, VarBaseJson} from './events_var_base.js'; import * as eventUtils from './utils.js'; @@ -28,14 +31,14 @@ export class VarDelete extends VarBase { /** * @param opt_variable The deleted variable. Undefined for a blank event. */ - constructor(opt_variable?: VariableModel) { + constructor(opt_variable?: IVariableModel) { super(opt_variable); if (!opt_variable) { return; // Blank event to be populated by fromJson. } - this.varType = opt_variable.type; - this.varName = opt_variable.name; + this.varType = opt_variable.getType(); + this.varName = opt_variable.getName(); } /** diff --git a/core/events/events_var_rename.ts b/core/events/events_var_rename.ts index 3bb1e90eb56..0de56c544f2 100644 --- a/core/events/events_var_rename.ts +++ b/core/events/events_var_rename.ts @@ -7,7 +7,10 @@ // Former goog.module ID: Blockly.Events.VarRename import * as registry from '../registry.js'; -import type {VariableModel} from '../variable_model.js'; +import type { + IVariableModel, + IVariableState, +} from '../interfaces/i_variable_model.js'; import {VarBase, VarBaseJson} from './events_var_base.js'; import * as eventUtils from './utils.js'; @@ -31,13 +34,13 @@ export class VarRename extends VarBase { * @param opt_variable The renamed variable. Undefined for a blank event. * @param newName The new name the variable will be changed to. */ - constructor(opt_variable?: VariableModel, newName?: string) { + constructor(opt_variable?: IVariableModel, newName?: string) { super(opt_variable); if (!opt_variable) { return; // Blank event to be populated by fromJson. } - this.oldName = opt_variable.name; + this.oldName = opt_variable.getName(); this.newName = typeof newName === 'undefined' ? '' : newName; } diff --git a/core/field_variable.ts b/core/field_variable.ts index d0a929bf014..a40a21cccd3 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -30,7 +30,7 @@ import type {MenuItem} from './menuitem.js'; import {Msg} from './msg.js'; import * as parsing from './utils/parsing.js'; import {Size} from './utils/size.js'; -import {VariableModel} from './variable_model.js'; +import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import * as Variables from './variables.js'; import * as Xml from './xml.js'; @@ -52,7 +52,7 @@ export class FieldVariable extends FieldDropdown { protected override size_: Size; /** The variable model associated with this field. */ - private variable: VariableModel | null = null; + private variable: IVariableModel | null = null; /** * Serializable fields are saved by the serializer, non-serializable fields @@ -196,12 +196,12 @@ export class FieldVariable extends FieldDropdown { ); // This should never happen :) - if (variableType !== null && variableType !== variable.type) { + if (variableType !== null && variableType !== variable.getType()) { throw Error( "Serialized variable type with id '" + variable.getId() + "' had type " + - variable.type + + variable.getType() + ', and ' + 'does not match variable field that references it: ' + Xml.domToText(fieldElement) + @@ -224,9 +224,9 @@ export class FieldVariable extends FieldDropdown { this.initModel(); fieldElement.id = this.variable!.getId(); - fieldElement.textContent = this.variable!.name; - if (this.variable!.type) { - fieldElement.setAttribute('variabletype', this.variable!.type); + fieldElement.textContent = this.variable!.getName(); + if (this.variable!.getType()) { + fieldElement.setAttribute('variabletype', this.variable!.getType()); } return fieldElement; } @@ -249,8 +249,8 @@ export class FieldVariable extends FieldDropdown { this.initModel(); const state = {'id': this.variable!.getId()}; if (doFullSerialization) { - (state as AnyDuringMigration)['name'] = this.variable!.name; - (state as AnyDuringMigration)['type'] = this.variable!.type; + (state as AnyDuringMigration)['name'] = this.variable!.getName(); + (state as AnyDuringMigration)['type'] = this.variable!.getType(); } return state; } @@ -307,7 +307,7 @@ export class FieldVariable extends FieldDropdown { * is selected. */ override getText(): string { - return this.variable ? this.variable.name : ''; + return this.variable ? this.variable.getName() : ''; } /** @@ -318,7 +318,7 @@ export class FieldVariable extends FieldDropdown { * @returns The selected variable, or null if none was selected. * @internal */ - getVariable(): VariableModel | null { + getVariable(): IVariableModel | null { return this.variable; } @@ -365,7 +365,7 @@ export class FieldVariable extends FieldDropdown { return null; } // Type Checks. - const type = variable.type; + const type = variable.getType(); if (!this.typeIsAllowed(type)) { console.warn("Variable type doesn't match this field! Type was " + type); return null; @@ -499,16 +499,13 @@ export class FieldVariable extends FieldDropdown { const id = menuItem.getValue(); // Handle special cases. if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) { - if (id === internalConstants.RENAME_VARIABLE_ID) { + if (id === internalConstants.RENAME_VARIABLE_ID && this.variable) { // Rename variable. - Variables.renameVariable( - this.sourceBlock_.workspace, - this.variable as VariableModel, - ); + Variables.renameVariable(this.sourceBlock_.workspace, this.variable); return; - } else if (id === internalConstants.DELETE_VARIABLE_ID) { + } else if (id === internalConstants.DELETE_VARIABLE_ID && this.variable) { // Delete variable. - this.sourceBlock_.workspace.deleteVariableById(this.variable!.getId()); + this.sourceBlock_.workspace.deleteVariableById(this.variable.getId()); return; } } @@ -560,7 +557,7 @@ export class FieldVariable extends FieldDropdown { ); } const name = this.getText(); - let variableModelList: VariableModel[] = []; + let variableModelList: IVariableModel[] = []; if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) { const variableTypes = this.getVariableTypes(); // Get a copy of the list, so that adding rename and new variable options @@ -572,12 +569,15 @@ export class FieldVariable extends FieldDropdown { variableModelList = variableModelList.concat(variables); } } - variableModelList.sort(VariableModel.compareByName); + variableModelList.sort(Variables.compareByName); const options: [string, string][] = []; for (let i = 0; i < variableModelList.length; i++) { // Set the UUID as the internal representation of the variable. - options[i] = [variableModelList[i].name, variableModelList[i].getId()]; + options[i] = [ + variableModelList[i].getName(), + variableModelList[i].getId(), + ]; } options.push([ Msg['RENAME_VARIABLE'], diff --git a/core/interfaces/i_variable_backed_parameter_model.ts b/core/interfaces/i_variable_backed_parameter_model.ts index b2042bfb2f5..4fda2df4660 100644 --- a/core/interfaces/i_variable_backed_parameter_model.ts +++ b/core/interfaces/i_variable_backed_parameter_model.ts @@ -4,13 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {VariableModel} from '../variable_model.js'; +import type {IVariableModel, IVariableState} from './i_variable_model.js'; import {IParameterModel} from './i_parameter_model.js'; /** Interface for a parameter model that holds a variable model. */ export interface IVariableBackedParameterModel extends IParameterModel { /** Returns the variable model held by this type. */ - getVariableModel(): VariableModel; + getVariableModel(): IVariableModel; } /** diff --git a/core/names.ts b/core/names.ts index 4f4c72faac8..9d724bff976 100644 --- a/core/names.ts +++ b/core/names.ts @@ -95,7 +95,7 @@ export class Names { } const variable = this.variableMap.getVariableById(id); if (variable) { - return variable.name; + return variable.getName(); } return null; } diff --git a/core/serialization/blocks.ts b/core/serialization/blocks.ts index dbb58cffb2a..355e53cacec 100644 --- a/core/serialization/blocks.ts +++ b/core/serialization/blocks.ts @@ -30,7 +30,10 @@ import { import * as priorities from './priorities.js'; import * as serializationRegistry from './registry.js'; import * as Variables from '../variables.js'; -import {VariableModel} from '../variable_model.js'; +import type { + IVariableModel, + IVariableState, +} from '../interfaces/i_variable_model.js'; // TODO(#5160): Remove this once lint is fixed. /* eslint-disable no-use-before-define */ @@ -503,7 +506,7 @@ function appendPrivate( */ function checkNewVariables( workspace: Workspace, - originalVariables: VariableModel[], + originalVariables: IVariableModel[], ) { if (eventUtils.isEnabled()) { const newVariables = Variables.getAddedVariables( diff --git a/core/variable_map.ts b/core/variable_map.ts index b8e2e4e0af6..1483e0c371e 100644 --- a/core/variable_map.ts +++ b/core/variable_map.ts @@ -23,7 +23,7 @@ import * as registry from './registry.js'; import {Msg} from './msg.js'; import {Names} from './names.js'; import * as idGenerator from './utils/idgenerator.js'; -import {VariableModel} from './variable_model.js'; +import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import type {Workspace} from './workspace.js'; import type {IVariableMap} from './interfaces/i_variable_map.js'; @@ -32,13 +32,18 @@ import type {IVariableMap} from './interfaces/i_variable_map.js'; * variable types as keys and lists of variables as values. The list of * variables are the type indicated by the key. */ -export class VariableMap implements IVariableMap { +export class VariableMap + implements IVariableMap> +{ /** * A map from variable type to map of IDs to variables. The maps contain * all of the named variables in the workspace, including variables that are * not currently in use. */ - private variableMap = new Map>(); + private variableMap = new Map< + string, + Map> + >(); /** @param workspace The workspace this map belongs to. */ constructor(public workspace: Workspace) {} @@ -63,9 +68,12 @@ export class VariableMap implements IVariableMap { * @param newName New variable name. * @returns The newly renamed variable. */ - renameVariable(variable: VariableModel, newName: string): VariableModel { - if (variable.name === newName) return variable; - const type = variable.type; + renameVariable( + variable: IVariableModel, + newName: string, + ): IVariableModel { + if (variable.getName() === newName) return variable; + const type = variable.getType(); const conflictVar = this.getVariable(newName, type); const blocks = this.workspace.getAllBlocks(false); const existingGroup = eventUtils.getGroup(); @@ -91,11 +99,15 @@ export class VariableMap implements IVariableMap { return variable; } - changeVariableType(variable: VariableModel, newType: string): VariableModel { + changeVariableType( + variable: IVariableModel, + newType: string, + ): IVariableModel { this.variableMap.get(variable.getType())?.delete(variable.getId()); variable.setType(newType); const newTypeVariables = - this.variableMap.get(newType) ?? new Map(); + this.variableMap.get(newType) ?? + new Map>(); newTypeVariables.set(variable.getId(), variable); if (!this.variableMap.has(newType)) { this.variableMap.set(newType, newTypeVariables); @@ -129,14 +141,14 @@ export class VariableMap implements IVariableMap { * @param blocks The list of all blocks in the workspace. */ private renameVariableAndUses_( - variable: VariableModel, + variable: IVariableModel, newName: string, blocks: Block[], ) { eventUtils.fire( new (eventUtils.get(eventUtils.VAR_RENAME))(variable, newName), ); - variable.name = newName; + variable.setName(newName); for (let i = 0; i < blocks.length; i++) { blocks[i].updateVarName(variable); } @@ -154,13 +166,13 @@ export class VariableMap implements IVariableMap { * @param blocks The list of all blocks in the workspace. */ private renameVariableWithConflict_( - variable: VariableModel, + variable: IVariableModel, newName: string, - conflictVar: VariableModel, + conflictVar: IVariableModel, blocks: Block[], ) { - const type = variable.type; - const oldCase = conflictVar.name; + const type = variable.getType(); + const oldCase = conflictVar.getName(); if (newName !== oldCase) { // Simple rename to change the case and update references. @@ -194,7 +206,7 @@ export class VariableMap implements IVariableMap { name: string, opt_type?: string, opt_id?: string, - ): VariableModel { + ): IVariableModel { let variable = this.getVariable(name, opt_type); if (variable) { if (opt_id && variable.getId() !== opt_id) { @@ -217,10 +229,19 @@ export class VariableMap implements IVariableMap { } const id = opt_id || idGenerator.genUid(); const type = opt_type || ''; + const VariableModel = registry.getObject( + registry.Type.VARIABLE_MODEL, + registry.DEFAULT, + true, + ); + if (!VariableModel) { + throw new Error('No variable model is registered.'); + } variable = new VariableModel(this.workspace, name, type, id); const variables = - this.variableMap.get(type) ?? new Map(); + this.variableMap.get(type) ?? + new Map>(); variables.set(variable.getId(), variable); if (!this.variableMap.has(type)) { this.variableMap.set(type, variables); @@ -235,10 +256,13 @@ export class VariableMap implements IVariableMap { * * @param variable The variable to add. */ - addVariable(variable: VariableModel) { + addVariable(variable: IVariableModel) { const type = variable.getType(); if (!this.variableMap.has(type)) { - this.variableMap.set(type, new Map()); + this.variableMap.set( + type, + new Map>(), + ); } this.variableMap.get(type)?.set(variable.getId(), variable); } @@ -249,13 +273,13 @@ export class VariableMap implements IVariableMap { * * @param variable Variable to delete. */ - deleteVariable(variable: VariableModel) { - const variables = this.variableMap.get(variable.type); + deleteVariable(variable: IVariableModel) { + const variables = this.variableMap.get(variable.getType()); if (!variables || !variables.has(variable.getId())) return; variables.delete(variable.getId()); eventUtils.fire(new (eventUtils.get(eventUtils.VAR_DELETE))(variable)); if (variables.size === 0) { - this.variableMap.delete(variable.type); + this.variableMap.delete(variable.getType()); } } @@ -269,7 +293,7 @@ export class VariableMap implements IVariableMap { const variable = this.getVariableById(id); if (variable) { // Check whether this variable is a function parameter before deleting. - const variableName = variable.name; + const variableName = variable.getName(); const uses = this.getVariableUsesById(id); for (let i = 0, block; (block = uses[i]); i++) { if ( @@ -312,7 +336,10 @@ export class VariableMap implements IVariableMap { * @param uses An array of uses of the variable. * @internal */ - deleteVariableInternal(variable: VariableModel, uses: Block[]) { + deleteVariableInternal( + variable: IVariableModel, + uses: Block[], + ) { const existingGroup = eventUtils.getGroup(); if (!existingGroup) { eventUtils.setGroup(true); @@ -336,7 +363,10 @@ export class VariableMap implements IVariableMap { * the empty string, which is a specific type. * @returns The variable with the given name, or null if it was not found. */ - getVariable(name: string, opt_type?: string): VariableModel | null { + getVariable( + name: string, + opt_type?: string, + ): IVariableModel | null { const type = opt_type || ''; const variables = this.variableMap.get(type); if (!variables) return null; @@ -354,7 +384,7 @@ export class VariableMap implements IVariableMap { * @param id The ID to check for. * @returns The variable with the given ID. */ - getVariableById(id: string): VariableModel | null { + getVariableById(id: string): IVariableModel | null { for (const variables of this.variableMap.values()) { if (variables.has(id)) { return variables.get(id) ?? null; @@ -371,7 +401,7 @@ export class VariableMap implements IVariableMap { * @returns The sought after variables of the passed in type. An empty array * if none are found. */ - getVariablesOfType(type: string | null): VariableModel[] { + getVariablesOfType(type: string | null): IVariableModel[] { type = type || ''; const variables = this.variableMap.get(type); if (!variables) return []; @@ -416,8 +446,8 @@ export class VariableMap implements IVariableMap { * * @returns List of variable models. */ - getAllVariables(): VariableModel[] { - let allVariables: VariableModel[] = []; + getAllVariables(): IVariableModel[] { + let allVariables: IVariableModel[] = []; for (const variables of this.variableMap.values()) { allVariables = allVariables.concat(...variables.values()); } diff --git a/core/variable_model.ts b/core/variable_model.ts index 017b02c6093..15ad5abf96c 100644 --- a/core/variable_model.ts +++ b/core/variable_model.ts @@ -26,7 +26,7 @@ import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; * @see {Blockly.FieldVariable} */ export class VariableModel implements IVariableModel { - type: string; + private type: string; private readonly id_: string; /** @@ -39,8 +39,8 @@ export class VariableModel implements IVariableModel { * @param opt_id The unique ID of the variable. This will default to a UUID. */ constructor( - public workspace: Workspace, - public name: string, + private workspace: Workspace, + private name: string, opt_type?: string, opt_id?: string, ) { @@ -130,7 +130,9 @@ export class VariableModel implements IVariableModel { * @internal */ static compareByName(var1: VariableModel, var2: VariableModel): number { - return var1.name.localeCompare(var2.name, undefined, {sensitivity: 'base'}); + return var1 + .getName() + .localeCompare(var2.getName(), undefined, {sensitivity: 'base'}); } } diff --git a/core/variables.ts b/core/variables.ts index da9d28bffc7..9809feca253 100644 --- a/core/variables.ts +++ b/core/variables.ts @@ -12,7 +12,7 @@ import {isVariableBackedParameterModel} from './interfaces/i_variable_backed_par import {Msg} from './msg.js'; import {isLegacyProcedureDefBlock} from './interfaces/i_legacy_procedure_blocks.js'; import * as utilsXml from './utils/xml.js'; -import {VariableModel} from './variable_model.js'; +import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import type {Workspace} from './workspace.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -34,9 +34,11 @@ export const CATEGORY_NAME = 'VARIABLE'; * @param ws The workspace to search for variables. * @returns Array of variable models. */ -export function allUsedVarModels(ws: Workspace): VariableModel[] { +export function allUsedVarModels( + ws: Workspace, +): IVariableModel[] { const blocks = ws.getAllBlocks(false); - const variables = new Set(); + const variables = new Set>(); // Iterate through every block and add each variable to the set. for (let i = 0; i < blocks.length; i++) { const blockVariables = blocks[i].getVarModels(); @@ -142,7 +144,7 @@ export function flyoutCategoryBlocks(workspace: Workspace): Element[] { } if (Blocks['variables_get']) { - variableModelList.sort(VariableModel.compareByName); + variableModelList.sort(compareByName); for (let i = 0, variable; (variable = variableModelList[i]); i++) { const block = utilsXml.createElement('block'); block.setAttribute('type', 'variables_get'); @@ -266,11 +268,13 @@ export function createVariableButtonHandler( } let msg; - if (existing.type === type) { - msg = Msg['VARIABLE_ALREADY_EXISTS'].replace('%1', existing.name); + if (existing.getType() === type) { + msg = Msg['VARIABLE_ALREADY_EXISTS'].replace('%1', existing.getName()); } else { msg = Msg['VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE']; - msg = msg.replace('%1', existing.name).replace('%2', existing.type); + msg = msg + .replace('%1', existing.getName()) + .replace('%2', existing.getType()); } dialog.alert(msg, function () { promptAndCheckWithAlert(text); @@ -293,14 +297,14 @@ export function createVariableButtonHandler( */ export function renameVariable( workspace: Workspace, - variable: VariableModel, + variable: IVariableModel, opt_callback?: (p1?: string | null) => void, ) { // This function needs to be named so it can be called recursively. function promptAndCheckWithAlert(defaultName: string) { const promptText = Msg['RENAME_VARIABLE_TITLE'].replace( '%1', - variable.name, + variable.getName(), ); promptName(promptText, defaultName, function (newName) { if (!newName) { @@ -309,9 +313,13 @@ export function renameVariable( return; } - const existing = nameUsedWithOtherType(newName, variable.type, workspace); + const existing = nameUsedWithOtherType( + newName, + variable.getType(), + workspace, + ); const procedure = nameUsedWithConflictingParam( - variable.name, + variable.getName(), newName, workspace, ); @@ -325,8 +333,8 @@ export function renameVariable( let msg = ''; if (existing) { msg = Msg['VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE'] - .replace('%1', existing.name) - .replace('%2', existing.type); + .replace('%1', existing.getName()) + .replace('%2', existing.getType()); } else if (procedure) { msg = Msg['VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER'] .replace('%1', newName) @@ -380,12 +388,15 @@ function nameUsedWithOtherType( name: string, type: string, workspace: Workspace, -): VariableModel | null { +): IVariableModel | null { const allVariables = workspace.getVariableMap().getAllVariables(); name = name.toLowerCase(); for (let i = 0, variable; (variable = allVariables[i]); i++) { - if (variable.name.toLowerCase() === name && variable.type !== type) { + if ( + variable.getName().toLowerCase() === name && + variable.getType() !== type + ) { return variable; } } @@ -402,12 +413,12 @@ function nameUsedWithOtherType( export function nameUsedWithAnyType( name: string, workspace: Workspace, -): VariableModel | null { +): IVariableModel | null { const allVariables = workspace.getVariableMap().getAllVariables(); name = name.toLowerCase(); for (let i = 0, variable; (variable = allVariables[i]); i++) { - if (variable.name.toLowerCase() === name) { + if (variable.getName().toLowerCase() === name) { return variable; } } @@ -453,7 +464,7 @@ function checkForConflictingParamWithProcedureModels( const params = procedure .getParameters() .filter(isVariableBackedParameterModel) - .map((param) => param.getVariableModel().name); + .map((param) => param.getVariableModel().getName()); if (!params) continue; const procHasOld = params.some((param) => param.toLowerCase() === oldName); const procHasNew = params.some((param) => param.toLowerCase() === newName); @@ -493,7 +504,7 @@ function checkForConflictingParamWithLegacyProcedures( * @returns The generated DOM. */ export function generateVariableFieldDom( - variableModel: VariableModel, + variableModel: IVariableModel, ): Element { /* Generates the following XML: * foo @@ -501,8 +512,8 @@ export function generateVariableFieldDom( const field = utilsXml.createElement('field'); field.setAttribute('name', 'VAR'); field.setAttribute('id', variableModel.getId()); - field.setAttribute('variabletype', variableModel.type); - const name = utilsXml.createTextNode(variableModel.name); + field.setAttribute('variabletype', variableModel.getType()); + const name = utilsXml.createTextNode(variableModel.getName()); field.appendChild(name); return field; } @@ -524,7 +535,7 @@ export function getOrCreateVariablePackage( id: string | null, opt_name?: string, opt_type?: string, -): VariableModel { +): IVariableModel { let variable = getVariable(workspace, id, opt_name, opt_type); if (!variable) { variable = createVariable(workspace, id, opt_name, opt_type); @@ -552,7 +563,7 @@ export function getVariable( id: string | null, opt_name?: string, opt_type?: string, -): VariableModel | null { +): IVariableModel | null { const potentialVariableMap = workspace.getPotentialVariableMap(); let variable = null; // Try to just get the variable, by ID if possible. @@ -597,7 +608,7 @@ function createVariable( id: string | null, opt_name?: string, opt_type?: string, -): VariableModel { +): IVariableModel { const potentialVariableMap = workspace.getPotentialVariableMap(); // Variables without names get uniquely named for this workspace. if (!opt_name) { @@ -637,8 +648,8 @@ function createVariable( */ export function getAddedVariables( workspace: Workspace, - originalVariables: VariableModel[], -): VariableModel[] { + originalVariables: IVariableModel[], +): IVariableModel[] { const allCurrentVariables = workspace.getAllVariables(); const addedVariables = []; if (originalVariables.length !== allCurrentVariables.length) { @@ -654,6 +665,24 @@ export function getAddedVariables( return addedVariables; } +/** + * A custom compare function for the VariableModel objects. + * + * @param var1 First variable to compare. + * @param var2 Second variable to compare. + * @returns -1 if name of var1 is less than name of var2, 0 if equal, and 1 if + * greater. + * @internal + */ +export function compareByName( + var1: IVariableModel, + var2: IVariableModel, +): number { + return var1 + .getName() + .localeCompare(var2.getName(), undefined, {sensitivity: 'base'}); +} + export const TEST_ONLY = { generateUniqueNameInternal, }; diff --git a/core/variables_dynamic.ts b/core/variables_dynamic.ts index 6c44575490f..5f1e2492c8c 100644 --- a/core/variables_dynamic.ts +++ b/core/variables_dynamic.ts @@ -9,7 +9,6 @@ import {Blocks} from './blocks.js'; import {Msg} from './msg.js'; import * as xml from './utils/xml.js'; -import {VariableModel} from './variable_model.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -129,7 +128,7 @@ export function flyoutCategoryBlocks(workspace: Workspace): Element[] { xmlList.push(block); } if (Blocks['variables_get_dynamic']) { - variableModelList.sort(VariableModel.compareByName); + variableModelList.sort(Variables.compareByName); for (let i = 0, variable; (variable = variableModelList[i]); i++) { const block = xml.createElement('block'); block.setAttribute('type', 'variables_get_dynamic'); diff --git a/core/workspace.ts b/core/workspace.ts index 71a2e4af9f6..063144995ee 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -29,7 +29,10 @@ import * as idGenerator from './utils/idgenerator.js'; import * as math from './utils/math.js'; import type * as toolbox from './utils/toolbox.js'; import {VariableMap} from './variable_map.js'; -import type {VariableModel} from './variable_model.js'; +import type { + IVariableModel, + IVariableState, +} from './interfaces/i_variable_model.js'; import {WorkspaceComment} from './comments/workspace_comment.js'; import {IProcedureMap} from './interfaces/i_procedure_map.js'; import {ObservableProcedureMap} from './observable_procedure_map.js'; @@ -399,7 +402,7 @@ export class Workspace implements IASTNodeLocation { name: string, opt_type?: string | null, opt_id?: string | null, - ): VariableModel { + ): IVariableModel { return this.variableMap.createVariable( name, opt_type ?? undefined, @@ -436,7 +439,10 @@ export class Workspace implements IASTNodeLocation { * the empty string, which is a specific type. * @returns The variable with the given name. */ - getVariable(name: string, opt_type?: string): VariableModel | null { + getVariable( + name: string, + opt_type?: string, + ): IVariableModel | null { // TODO (#1559): Possibly delete this function after resolving #1559. return this.variableMap.getVariable(name, opt_type); } @@ -447,7 +453,7 @@ export class Workspace implements IASTNodeLocation { * @param id The ID to check for. * @returns The variable with the given ID. */ - getVariableById(id: string): VariableModel | null { + getVariableById(id: string): IVariableModel | null { return this.variableMap.getVariableById(id); } @@ -459,7 +465,7 @@ export class Workspace implements IASTNodeLocation { * @returns The sought after variables of the passed in type. An empty array * if none are found. */ - getVariablesOfType(type: string | null): VariableModel[] { + getVariablesOfType(type: string | null): IVariableModel[] { return this.variableMap.getVariablesOfType(type ?? ''); } @@ -478,7 +484,7 @@ export class Workspace implements IASTNodeLocation { * * @returns List of variable models. */ - getAllVariables(): VariableModel[] { + getAllVariables(): IVariableModel[] { return this.variableMap.getAllVariables(); } diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index aad748105f0..55a7540bd85 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -62,7 +62,10 @@ import {Svg} from './utils/svg.js'; import * as svgMath from './utils/svg_math.js'; import * as toolbox from './utils/toolbox.js'; import * as userAgent from './utils/useragent.js'; -import type {VariableModel} from './variable_model.js'; +import type { + IVariableModel, + IVariableState, +} from './interfaces/i_variable_model.js'; import * as Variables from './variables.js'; import * as VariablesDynamic from './variables_dynamic.js'; import * as WidgetDiv from './widgetdiv.js'; @@ -1354,7 +1357,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { name: string, opt_type?: string | null, opt_id?: string | null, - ): VariableModel { + ): IVariableModel { const newVar = super.createVariable(name, opt_type, opt_id); this.refreshToolboxSelection(); return newVar; diff --git a/core/xml.ts b/core/xml.ts index b8ecf6433d8..48d43c6f00e 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -17,7 +17,10 @@ import {inputTypes} from './inputs/input_types.js'; import * as dom from './utils/dom.js'; import {Size} from './utils/size.js'; import * as utilsXml from './utils/xml.js'; -import type {VariableModel} from './variable_model.js'; +import type { + IVariableModel, + IVariableState, +} from './interfaces/i_variable_model.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; import {WorkspaceSvg} from './workspace_svg.js'; @@ -86,14 +89,16 @@ export function saveWorkspaceComment( * @param variableList List of all variable models. * @returns Tree of XML elements. */ -export function variablesToDom(variableList: VariableModel[]): Element { +export function variablesToDom( + variableList: IVariableModel[], +): Element { const variables = utilsXml.createElement('variables'); for (let i = 0; i < variableList.length; i++) { const variable = variableList[i]; const element = utilsXml.createElement('variable'); - element.appendChild(utilsXml.createTextNode(variable.name)); - if (variable.type) { - element.setAttribute('type', variable.type); + element.appendChild(utilsXml.createTextNode(variable.getName())); + if (variable.getType()) { + element.setAttribute('type', variable.getType()); } element.id = variable.getId(); variables.appendChild(element); diff --git a/generators/php/procedures.ts b/generators/php/procedures.ts index acf84aea658..9e3edd31f75 100644 --- a/generators/php/procedures.ts +++ b/generators/php/procedures.ts @@ -25,7 +25,7 @@ export function procedures_defreturn(block: Block, generator: PhpGenerator) { const workspace = block.workspace; const usedVariables = Variables.allUsedVarModels(workspace) || []; for (const variable of usedVariables) { - const varName = variable.name; + const varName = variable.getName(); // getVars returns parameter names, not ids, for procedure blocks if (!block.getVars().includes(varName)) { globals.push(generator.getVariableName(varName)); diff --git a/generators/python/procedures.ts b/generators/python/procedures.ts index 51d2ee9a31b..39c50698bc6 100644 --- a/generators/python/procedures.ts +++ b/generators/python/procedures.ts @@ -25,7 +25,7 @@ export function procedures_defreturn(block: Block, generator: PythonGenerator) { const workspace = block.workspace; const usedVariables = Variables.allUsedVarModels(workspace) || []; for (const variable of usedVariables) { - const varName = variable.name; + const varName = variable.getName(); // getVars returns parameter names, not ids, for procedure blocks if (!block.getVars().includes(varName)) { globals.push(generator.getVariableName(varName)); diff --git a/tests/mocha/xml_test.js b/tests/mocha/xml_test.js index c3ca2d4162e..d30716edb44 100644 --- a/tests/mocha/xml_test.js +++ b/tests/mocha/xml_test.js @@ -922,6 +922,12 @@ suite('XML', function () { getId: function () { return varId; }, + getName: function () { + return name; + }, + getType: function () { + return type; + }, }; const generatedXml = Blockly.Xml.domToText( From 21c0a7d9998730717ed0bde24706404552073302 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 22 Jul 2024 09:17:40 -0700 Subject: [PATCH 025/222] refactor!: Use IVariableMap instead of VariableMap (#8401) * refactor: use IVariableMap in place of VariableMap. * refactor!: move variable deletion prompting out of VariableMap. * chore: Remove unused imports. --- core/names.ts | 12 ++- core/variable_map.ts | 125 ++++++++++--------------------- core/variables.ts | 69 +++++++++++++++++ core/workspace.ts | 55 +++++++++++--- tests/mocha/variable_map_test.js | 2 +- 5 files changed, 161 insertions(+), 102 deletions(-) diff --git a/core/names.ts b/core/names.ts index 9d724bff976..9976da224d2 100644 --- a/core/names.ts +++ b/core/names.ts @@ -12,8 +12,11 @@ // Former goog.module ID: Blockly.Names import {Msg} from './msg.js'; -// import * as Procedures from './procedures.js'; -import type {VariableMap} from './variable_map.js'; +import type {IVariableMap} from './interfaces/i_variable_map.js'; +import type { + IVariableModel, + IVariableState, +} from './interfaces/i_variable_model.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; @@ -39,7 +42,8 @@ export class Names { /** * The variable map from the workspace, containing Blockly variable models. */ - private variableMap: VariableMap | null = null; + private variableMap: IVariableMap> | null = + null; /** * @param reservedWordsList A comma-separated string of words that are illegal @@ -70,7 +74,7 @@ export class Names { * * @param map The map to track. */ - setVariableMap(map: VariableMap) { + setVariableMap(map: IVariableMap>) { this.variableMap = map; } diff --git a/core/variable_map.ts b/core/variable_map.ts index 1483e0c371e..dd59311dc8d 100644 --- a/core/variable_map.ts +++ b/core/variable_map.ts @@ -17,10 +17,10 @@ import './events/events_var_delete.js'; import './events/events_var_rename.js'; import type {Block} from './block.js'; -import * as dialog from './dialog.js'; +import * as deprecation from './utils/deprecation.js'; import * as eventUtils from './events/utils.js'; import * as registry from './registry.js'; -import {Msg} from './msg.js'; +import * as Variables from './variables.js'; import {Names} from './names.js'; import * as idGenerator from './utils/idgenerator.js'; import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; @@ -247,7 +247,6 @@ export class VariableMap this.variableMap.set(type, variables); } eventUtils.fire(new (eventUtils.get(eventUtils.VAR_CREATE))(variable)); - return variable; } @@ -269,90 +268,51 @@ export class VariableMap /* Begin functions for variable deletion. */ /** - * Delete a variable. + * Delete a variable and all of its uses without confirmation. * * @param variable Variable to delete. */ deleteVariable(variable: IVariableModel) { - const variables = this.variableMap.get(variable.getType()); - if (!variables || !variables.has(variable.getId())) return; - variables.delete(variable.getId()); - eventUtils.fire(new (eventUtils.get(eventUtils.VAR_DELETE))(variable)); - if (variables.size === 0) { - this.variableMap.delete(variable.getType()); + const uses = this.getVariableUsesById(variable.getId()); + const existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } + try { + for (let i = 0; i < uses.length; i++) { + uses[i].dispose(true); + } + const variables = this.variableMap.get(variable.getType()); + if (!variables || !variables.has(variable.getId())) return; + variables.delete(variable.getId()); + eventUtils.fire(new (eventUtils.get(eventUtils.VAR_DELETE))(variable)); + if (variables.size === 0) { + this.variableMap.delete(variable.getType()); + } + } finally { + eventUtils.setGroup(existingGroup); } } /** - * Delete a variables by the passed in ID and all of its uses from this - * workspace. May prompt the user for confirmation. + * @deprecated v12 - Delete a variables by the passed in ID and all of its + * uses from this workspace. May prompt the user for confirmation. * * @param id ID of variable to delete. */ deleteVariableById(id: string) { + deprecation.warn( + 'VariableMap.deleteVariableById', + 'v12', + 'v13', + 'Blockly.Variables.deleteVariable', + ); const variable = this.getVariableById(id); if (variable) { - // Check whether this variable is a function parameter before deleting. - const variableName = variable.getName(); - const uses = this.getVariableUsesById(id); - for (let i = 0, block; (block = uses[i]); i++) { - if ( - block.type === 'procedures_defnoreturn' || - block.type === 'procedures_defreturn' - ) { - const procedureName = String(block.getFieldValue('NAME')); - const deleteText = Msg['CANNOT_DELETE_VARIABLE_PROCEDURE'] - .replace('%1', variableName) - .replace('%2', procedureName); - dialog.alert(deleteText); - return; - } - } - - if (uses.length > 1) { - // Confirm before deleting multiple blocks. - const confirmText = Msg['DELETE_VARIABLE_CONFIRMATION'] - .replace('%1', String(uses.length)) - .replace('%2', variableName); - dialog.confirm(confirmText, (ok) => { - if (ok && variable) { - this.deleteVariableInternal(variable, uses); - } - }); - } else { - // No confirmation necessary for a single block. - this.deleteVariableInternal(variable, uses); - } - } else { - console.warn("Can't delete non-existent variable: " + id); + Variables.deleteVariable(this.workspace, variable); } } - /** - * Deletes a variable and all of its uses from this workspace without asking - * the user for confirmation. - * - * @param variable Variable to delete. - * @param uses An array of uses of the variable. - * @internal - */ - deleteVariableInternal( - variable: IVariableModel, - uses: Block[], - ) { - const existingGroup = eventUtils.getGroup(); - if (!existingGroup) { - eventUtils.setGroup(true); - } - try { - for (let i = 0; i < uses.length; i++) { - uses[i].dispose(true); - } - this.deleteVariable(variable); - } finally { - eventUtils.setGroup(existingGroup); - } - } /* End functions for variable deletion. */ /** * Find the variable by the given name and type and return it. Return null if @@ -431,7 +391,7 @@ export class VariableMap getVariableTypes(ws: Workspace | null): string[] { const variableTypes = new Set(this.variableMap.keys()); if (ws && ws.getPotentialVariableMap()) { - for (const key of ws.getPotentialVariableMap()!.variableMap.keys()) { + for (const key of ws.getPotentialVariableMap()!.getTypes()) { variableTypes.add(key); } } @@ -470,26 +430,19 @@ export class VariableMap } /** - * Find all the uses of a named variable. + * @deprecated v12 - Find all the uses of a named variable. * * @param id ID of the variable to find. * @returns Array of block usages. */ getVariableUsesById(id: string): Block[] { - const uses = []; - const blocks = this.workspace.getAllBlocks(false); - // Iterate through every block and check the name. - for (let i = 0; i < blocks.length; i++) { - const blockVariables = blocks[i].getVarModels(); - if (blockVariables) { - for (let j = 0; j < blockVariables.length; j++) { - if (blockVariables[j].getId() === id) { - uses.push(blocks[i]); - } - } - } - } - return uses; + deprecation.warn( + 'VariableMap.getVariableUsesById', + 'v12', + 'v13', + 'Blockly.Variables.getVariableUsesById', + ); + return Variables.getVariableUsesById(this.workspace, id); } } diff --git a/core/variables.ts b/core/variables.ts index 9809feca253..5da228f6cc2 100644 --- a/core/variables.ts +++ b/core/variables.ts @@ -6,6 +6,7 @@ // Former goog.module ID: Blockly.Variables +import type {Block} from './block.js'; import {Blocks} from './blocks.js'; import * as dialog from './dialog.js'; import {isVariableBackedParameterModel} from './interfaces/i_variable_backed_parameter_model.js'; @@ -683,6 +684,74 @@ export function compareByName( .localeCompare(var2.getName(), undefined, {sensitivity: 'base'}); } +/** + * Find all the uses of a named variable. + * + * @param workspace The workspace to search for the variable. + * @param id ID of the variable to find. + * @returns Array of block usages. + */ +export function getVariableUsesById(workspace: Workspace, id: string): Block[] { + const uses = []; + const blocks = workspace.getAllBlocks(false); + // Iterate through every block and check the name. + for (let i = 0; i < blocks.length; i++) { + const blockVariables = blocks[i].getVarModels(); + if (blockVariables) { + for (let j = 0; j < blockVariables.length; j++) { + if (blockVariables[j].getId() === id) { + uses.push(blocks[i]); + } + } + } + } + return uses; +} + +/** + * Delete a variable and all of its uses from the given workspace. May prompt + * the user for confirmation. + * + * @param workspace The workspace from which to delete the variable. + * @param variable The variable to delete. + */ +export function deleteVariable( + workspace: Workspace, + variable: IVariableModel, +) { + // Check whether this variable is a function parameter before deleting. + const variableName = variable.getName(); + const uses = getVariableUsesById(workspace, variable.getId()); + for (let i = 0, block; (block = uses[i]); i++) { + if ( + block.type === 'procedures_defnoreturn' || + block.type === 'procedures_defreturn' + ) { + const procedureName = String(block.getFieldValue('NAME')); + const deleteText = Msg['CANNOT_DELETE_VARIABLE_PROCEDURE'] + .replace('%1', variableName) + .replace('%2', procedureName); + dialog.alert(deleteText); + return; + } + } + + if (uses.length > 1) { + // Confirm before deleting multiple blocks. + const confirmText = Msg['DELETE_VARIABLE_CONFIRMATION'] + .replace('%1', String(uses.length)) + .replace('%2', variableName); + dialog.confirm(confirmText, (ok) => { + if (ok && variable) { + workspace.getVariableMap().deleteVariable(variable); + } + }); + } else { + // No confirmation necessary for a single block. + workspace.getVariableMap().deleteVariable(variable); + } +} + export const TEST_ONLY = { generateUniqueNameInternal, }; diff --git a/core/workspace.ts b/core/workspace.ts index 063144995ee..2981fc6ada5 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -28,7 +28,8 @@ import * as arrayUtils from './utils/array.js'; import * as idGenerator from './utils/idgenerator.js'; import * as math from './utils/math.js'; import type * as toolbox from './utils/toolbox.js'; -import {VariableMap} from './variable_map.js'; +import * as Variables from './variables.js'; +import type {IVariableMap} from './interfaces/i_variable_map.js'; import type { IVariableModel, IVariableState, @@ -110,7 +111,7 @@ export class Workspace implements IASTNodeLocation { protected redoStack_: Abstract[] = []; private readonly blockDB = new Map(); private readonly typedBlocksDB = new Map(); - private variableMap: VariableMap; + private variableMap: IVariableMap>; private procedureMap: IProcedureMap = new ObservableProcedureMap(); /** @@ -121,7 +122,9 @@ export class Workspace implements IASTNodeLocation { * these by tracking "potential" variables in the flyout. These variables * become real when references to them are dragged into the main workspace. */ - private potentialVariableMap: VariableMap | null = null; + private potentialVariableMap: IVariableMap< + IVariableModel + > | null = null; /** @param opt_options Dictionary of options. */ constructor(opt_options?: Options) { @@ -147,6 +150,7 @@ export class Workspace implements IASTNodeLocation { * all of the named variables in the workspace, including variables that are * not currently in use. */ + const VariableMap = this.getVariableMapClass(); this.variableMap = new VariableMap(this); } @@ -384,7 +388,9 @@ export class Workspace implements IASTNodeLocation { * @param newName New variable name. */ renameVariableById(id: string, newName: string) { - this.variableMap.renameVariableById(id, newName); + const variable = this.variableMap.getVariableById(id); + if (!variable) return; + this.variableMap.renameVariable(variable, newName); } /** @@ -417,7 +423,7 @@ export class Workspace implements IASTNodeLocation { * @returns Array of block usages. */ getVariableUsesById(id: string): Block[] { - return this.variableMap.getVariableUsesById(id); + return Variables.getVariableUsesById(this, id); } /** @@ -427,7 +433,12 @@ export class Workspace implements IASTNodeLocation { * @param id ID of variable to delete. */ deleteVariableById(id: string) { - this.variableMap.deleteVariableById(id); + const variable = this.variableMap.getVariableById(id); + if (!variable) { + console.warn(`Can't delete non-existent variable: ${id}`); + return; + } + Variables.deleteVariable(this, variable); } /** @@ -476,7 +487,12 @@ export class Workspace implements IASTNodeLocation { * @internal */ getVariableTypes(): string[] { - return this.variableMap.getVariableTypes(this); + const variableTypes = new Set(this.variableMap.getTypes()); + (this.potentialVariableMap?.getTypes() ?? []).forEach((t) => + variableTypes.add(t), + ); + variableTypes.add(''); + return Array.from(variableTypes.values()); } /** @@ -494,7 +510,7 @@ export class Workspace implements IASTNodeLocation { * @returns List of all variable names of all types. */ getAllVariableNames(): string[] { - return this.variableMap.getAllVariableNames(); + return this.variableMap.getAllVariables().map((v) => v.getName()); } /* End functions that are just pass-throughs to the variable map. */ /** @@ -789,7 +805,9 @@ export class Workspace implements IASTNodeLocation { * @returns The potential variable map. * @internal */ - getPotentialVariableMap(): VariableMap | null { + getPotentialVariableMap(): IVariableMap< + IVariableModel + > | null { return this.potentialVariableMap; } @@ -799,6 +817,7 @@ export class Workspace implements IASTNodeLocation { * @internal */ createPotentialVariableMap() { + const VariableMap = this.getVariableMapClass(); this.potentialVariableMap = new VariableMap(this); } @@ -807,7 +826,7 @@ export class Workspace implements IASTNodeLocation { * * @returns The variable map. */ - getVariableMap(): VariableMap { + getVariableMap(): IVariableMap> { return this.variableMap; } @@ -817,7 +836,7 @@ export class Workspace implements IASTNodeLocation { * @param variableMap The variable map. * @internal */ - setVariableMap(variableMap: VariableMap) { + setVariableMap(variableMap: IVariableMap>) { this.variableMap = variableMap; } @@ -866,4 +885,18 @@ export class Workspace implements IASTNodeLocation { static getAll(): Workspace[] { return common.getAllWorkspaces(); } + + protected getVariableMapClass(): new ( + ...p1: any[] + ) => IVariableMap> { + const VariableMap = registry.getClassFromOptions( + registry.Type.VARIABLE_MAP, + this.options, + true, + ); + if (!VariableMap) { + throw new Error('No variable map is registered.'); + } + return VariableMap; + } } diff --git a/tests/mocha/variable_map_test.js b/tests/mocha/variable_map_test.js index 13a474245df..76c1702cc71 100644 --- a/tests/mocha/variable_map_test.js +++ b/tests/mocha/variable_map_test.js @@ -21,7 +21,7 @@ suite('Variable Map', function () { setup(function () { sharedTestSetup.call(this); this.workspace = new Blockly.Workspace(); - this.variableMap = new Blockly.VariableMap(this.workspace); + this.variableMap = this.workspace.getVariableMap(); }); teardown(function () { From 26e6d80e155e1070425ca74f82d610355f4b7931 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 22 Jul 2024 10:51:56 -0700 Subject: [PATCH 026/222] refactor: clean up VariableModel. (#8416) --- core/variable_model.ts | 37 +++++++++++++++++++++++------- tests/mocha/variable_model_test.js | 28 +++++++++++----------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/core/variable_model.ts b/core/variable_model.ts index 15ad5abf96c..f298480843b 100644 --- a/core/variable_model.ts +++ b/core/variable_model.ts @@ -15,6 +15,7 @@ import './events/events_var_create.js'; import * as idGenerator from './utils/idgenerator.js'; +import * as eventUtils from './events/utils.js'; import * as registry from './registry.js'; import type {Workspace} from './workspace.js'; import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; @@ -27,7 +28,7 @@ import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; */ export class VariableModel implements IVariableModel { private type: string; - private readonly id_: string; + private readonly id: string; /** * @param workspace The variable's workspace. @@ -39,7 +40,7 @@ export class VariableModel implements IVariableModel { * @param opt_id The unique ID of the variable. This will default to a UUID. */ constructor( - private workspace: Workspace, + private readonly workspace: Workspace, private name: string, opt_type?: string, opt_id?: string, @@ -58,12 +59,12 @@ export class VariableModel implements IVariableModel { * not change, even if the name changes. In most cases this should be a * UUID. */ - this.id_ = opt_id || idGenerator.genUid(); + this.id = opt_id || idGenerator.genUid(); } /** @returns The ID for the variable. */ getId(): string { - return this.id_; + return this.id; } /** @returns The name of this variable. */ @@ -96,10 +97,20 @@ export class VariableModel implements IVariableModel { return this; } + /** + * Returns the workspace this VariableModel belongs to. + * + * @returns The workspace this VariableModel belongs to. + */ getWorkspace(): Workspace { return this.workspace; } + /** + * Serializes this VariableModel. + * + * @returns a JSON representation of this VariableModel. + */ save(): IVariableState { const state: IVariableState = { 'name': this.getName(), @@ -113,11 +124,21 @@ export class VariableModel implements IVariableModel { return state; } + /** + * Loads the persisted state into a new variable in the given workspace. + * + * @param state The serialized state of a variable model from save(). + * @param workspace The workspace to create the new variable in. + */ static load(state: IVariableState, workspace: Workspace) { - // TODO(adodson): Once VariableMap implements IVariableMap, directly - // construct a variable, retrieve the variable map from the workspace, - // add the variable to that variable map, and fire a VAR_CREATE event. - workspace.createVariable(state['name'], state['type'], state['id']); + const variable = new this( + workspace, + state['name'], + state['type'], + state['id'], + ); + workspace.getVariableMap().addVariable(variable); + eventUtils.fire(new (eventUtils.get(eventUtils.VAR_CREATE))(variable)); } /** diff --git a/tests/mocha/variable_model_test.js b/tests/mocha/variable_model_test.js index 207c580de51..cd2a89db420 100644 --- a/tests/mocha/variable_model_test.js +++ b/tests/mocha/variable_model_test.js @@ -27,9 +27,9 @@ suite('Variable Model', function () { 'test_type', 'test_id', ); - assert.equal(variable.name, 'test'); - assert.equal(variable.type, 'test_type'); - assert.equal(variable.id_, 'test_id'); + assert.equal(variable.getName(), 'test'); + assert.equal(variable.getType(), 'test_type'); + assert.equal(variable.getId(), 'test_id'); }); test('Null type', function () { @@ -39,7 +39,7 @@ suite('Variable Model', function () { null, 'test_id', ); - assert.equal(variable.type, ''); + assert.equal(variable.getType(), ''); }); test('Undefined type', function () { @@ -49,7 +49,7 @@ suite('Variable Model', function () { undefined, 'test_id', ); - assert.equal(variable.type, ''); + assert.equal(variable.getType(), ''); }); test('Null id', function () { @@ -59,9 +59,9 @@ suite('Variable Model', function () { 'test_type', null, ); - assert.equal(variable.name, 'test'); - assert.equal(variable.type, 'test_type'); - assert.exists(variable.id_); + assert.equal(variable.getName(), 'test'); + assert.equal(variable.getType(), 'test_type'); + assert.exists(variable.getId()); }); test('Undefined id', function () { @@ -71,15 +71,15 @@ suite('Variable Model', function () { 'test_type', undefined, ); - assert.equal(variable.name, 'test'); - assert.equal(variable.type, 'test_type'); - assert.exists(variable.id_); + assert.equal(variable.getName(), 'test'); + assert.equal(variable.getType(), 'test_type'); + assert.exists(variable.getId()); }); test('Only name provided', function () { const variable = new Blockly.VariableModel(this.workspace, 'test'); - assert.equal(variable.name, 'test'); - assert.equal(variable.type, ''); - assert.exists(variable.id_); + assert.equal(variable.getName(), 'test'); + assert.equal(variable.getType(), ''); + assert.exists(variable.getId()); }); }); From 58abf6ef892a452e71727d4adcddd52f6c836c86 Mon Sep 17 00:00:00 2001 From: Gabriel Fleury <55366345+ga-fleury@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:14:17 -0300 Subject: [PATCH 027/222] fix: Remove references to getFastTextWidth (#8277) (#8307) * feat: Remove references to getFastTextWidth (#8277) * format --- core/field.ts | 7 +------ core/field_dropdown.ts | 14 ++------------ 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/core/field.ts b/core/field.ts index eed34e613fc..68f4e2bb420 100644 --- a/core/field.ts +++ b/core/field.ts @@ -835,12 +835,7 @@ export abstract class Field let contentWidth = 0; if (this.textElement_) { - contentWidth = dom.getFastTextWidth( - this.textElement_, - constants!.FIELD_TEXT_FONTSIZE, - constants!.FIELD_TEXT_FONTWEIGHT, - constants!.FIELD_TEXT_FONTFAMILY, - ); + contentWidth = dom.getTextWidth(this.textElement_); totalWidth += contentWidth; } if (!this.isFullBlockField()) { diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 58a4b073218..5f26ac3b403 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -532,12 +532,7 @@ export class FieldDropdown extends Field { height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2, ); } else { - arrowWidth = dom.getFastTextWidth( - this.arrow as SVGTSpanElement, - this.getConstants()!.FIELD_TEXT_FONTSIZE, - this.getConstants()!.FIELD_TEXT_FONTWEIGHT, - this.getConstants()!.FIELD_TEXT_FONTFAMILY, - ); + arrowWidth = dom.getTextWidth(this.arrow as SVGTSpanElement); } this.size_.width = imageWidth + arrowWidth + xPadding * 2; this.size_.height = height; @@ -570,12 +565,7 @@ export class FieldDropdown extends Field { hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0, this.getConstants()!.FIELD_TEXT_HEIGHT, ); - const textWidth = dom.getFastTextWidth( - this.getTextElement(), - this.getConstants()!.FIELD_TEXT_FONTSIZE, - this.getConstants()!.FIELD_TEXT_FONTWEIGHT, - this.getConstants()!.FIELD_TEXT_FONTFAMILY, - ); + const textWidth = dom.getTextWidth(this.getTextElement()); const xPadding = hasBorder ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : 0; From 348313a1b6d371344ca2cefcd128de1ececb3d1b Mon Sep 17 00:00:00 2001 From: Gabriel Fleury <55366345+ga-fleury@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:14:45 -0300 Subject: [PATCH 028/222] feat: Add a blocklyCollapsed CSS class to collapsed blocks' root SVG (#8264) (#8308) * feat: Add a blocklyCollapsed CSS class to collapsed blocks' root SVG (#8264) * format --- core/block_svg.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/block_svg.ts b/core/block_svg.ts index 5e96657f436..5a059e47b8d 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -524,9 +524,12 @@ export class BlockSvg if (!collapsed) { this.updateDisabled(); this.removeInput(collapsedInputName); + dom.removeClass(this.svgGroup_, 'blocklyCollapsed'); return; } + dom.addClass(this.svgGroup_, 'blocklyCollapsed'); + const text = this.toString(internalConstants.COLLAPSE_CHARS); const field = this.getField(collapsedFieldName); if (field) { From e29d7abfdb706ed7db1dc14acd935c3c3950bdbc Mon Sep 17 00:00:00 2001 From: Gabriel Fleury <55366345+ga-fleury@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:38:40 -0300 Subject: [PATCH 029/222] fix!: Rename editing CSS class to blocklyEditing (#8287) (#8301) * chore!: Rename editing CSS class to blocklyEditing (#8287) * further changes --- core/field_input.ts | 4 ++-- core/renderers/common/constants.ts | 2 +- core/renderers/zelos/constants.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/field_input.ts b/core/field_input.ts index 85431cc5b33..3326cd35f48 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -405,7 +405,7 @@ export abstract class FieldInput extends Field< const clickTarget = this.getClickTarget_(); if (!clickTarget) throw new Error('A click target has not been set.'); - dom.addClass(clickTarget, 'editing'); + dom.addClass(clickTarget, 'blocklyEditing'); const htmlInput = document.createElement('input'); htmlInput.className = 'blocklyHtmlInput'; @@ -500,7 +500,7 @@ export abstract class FieldInput extends Field< const clickTarget = this.getClickTarget_(); if (!clickTarget) throw new Error('A click target has not been set.'); - dom.removeClass(clickTarget, 'editing'); + dom.removeClass(clickTarget, 'blocklyEditing'); } /** diff --git a/core/renderers/common/constants.ts b/core/renderers/common/constants.ts index 078fc01d648..568b7f444d5 100644 --- a/core/renderers/common/constants.ts +++ b/core/renderers/common/constants.ts @@ -1154,7 +1154,7 @@ export class ConstantProvider { `}`, // Editable field hover. - `${selector} .blocklyEditableText:not(.editing):hover>rect {`, + `${selector} .blocklyEditableText:not(.blocklyEditing):hover>rect {`, `stroke: #fff;`, `stroke-width: 2;`, `}`, diff --git a/core/renderers/zelos/constants.ts b/core/renderers/zelos/constants.ts index c50e66510a0..22e3f3782e9 100644 --- a/core/renderers/zelos/constants.ts +++ b/core/renderers/zelos/constants.ts @@ -825,9 +825,9 @@ export class ConstantProvider extends BaseConstantProvider { // Editable field hover. `${selector} .blocklyDraggable:not(.blocklyDisabled)`, - ` .blocklyEditableText:not(.editing):hover>rect,`, + ` .blocklyEditableText:not(.blocklyEditing):hover>rect,`, `${selector} .blocklyDraggable:not(.blocklyDisabled)`, - ` .blocklyEditableText:not(.editing):hover>.blocklyPath {`, + ` .blocklyEditableText:not(.blocklyEditing):hover>.blocklyPath {`, `stroke: #fff;`, `stroke-width: 2;`, `}`, From 76eebc2f24c0002a9ee1927249fae8c600244b8b Mon Sep 17 00:00:00 2001 From: Chaitanya Yeole <77329060+ChaitanyaYeole02@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:05:19 -0400 Subject: [PATCH 030/222] feat: Add a blocklyBlock CSS class to the block's root SVG (#8397) --- core/renderers/common/path_object.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index 8ca8cd19324..c5ac5db20f0 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -66,6 +66,8 @@ export class PathObject implements IPathObject { {'class': 'blocklyPath'}, this.svgRoot, ); + + this.setClass_('blocklyBlock', true); } /** From fb82c9c9bb4acb08a1a5a0c4bd72088e9c594295 Mon Sep 17 00:00:00 2001 From: Shreyans Pathak Date: Mon, 22 Jul 2024 19:09:10 -0400 Subject: [PATCH 031/222] feat: add `blocklyMiniWorkspaceBubble` css class (#8390) --- core/bubbles/mini_workspace_bubble.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/bubbles/mini_workspace_bubble.ts b/core/bubbles/mini_workspace_bubble.ts index 74317d57bc1..d11efb2809a 100644 --- a/core/bubbles/mini_workspace_bubble.ts +++ b/core/bubbles/mini_workspace_bubble.ts @@ -80,6 +80,7 @@ export class MiniWorkspaceBubble extends Bubble { flyout?.show(options.languageTree); } + dom.addClass(this.svgRoot, 'blocklyMiniWorkspaceBubble'); this.miniWorkspace.addChangeListener(this.onWorkspaceChange.bind(this)); this.miniWorkspace .getFlyout() From 91892ac303b28e56f145075437ef13ee83861b36 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 22 Jul 2024 17:13:20 -0700 Subject: [PATCH 032/222] refactor: deprecate and clean up variable-related methods. (#8415) * refactor: deprecate and clean up variable-related methods. * chore: Add deprecation JSDoc. --- core/field_variable.ts | 2 +- core/variable_map.ts | 60 +++++++++------------- core/variable_model.ts | 15 ------ core/workspace.ts | 82 +++++++++++++++++++++++------- tests/mocha/field_variable_test.js | 3 +- tests/mocha/variable_map_test.js | 18 ------- 6 files changed, 91 insertions(+), 89 deletions(-) diff --git a/core/field_variable.ts b/core/field_variable.ts index a40a21cccd3..9dbba4ca3c7 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -420,7 +420,7 @@ export class FieldVariable extends FieldDropdown { if (variableTypes === null) { // If variableTypes is null, return all variable types. if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) { - return this.sourceBlock_.workspace.getVariableTypes(); + return this.sourceBlock_.workspace.getVariableMap().getTypes(); } } variableTypes = variableTypes || ['']; diff --git a/core/variable_map.ts b/core/variable_map.ts index dd59311dc8d..c9e49b66444 100644 --- a/core/variable_map.ts +++ b/core/variable_map.ts @@ -84,14 +84,9 @@ export class VariableMap // The IDs may match if the rename is a simple case change (name1 -> // Name1). if (!conflictVar || conflictVar.getId() === variable.getId()) { - this.renameVariableAndUses_(variable, newName, blocks); + this.renameVariableAndUses(variable, newName, blocks); } else { - this.renameVariableWithConflict_( - variable, - newName, - conflictVar, - blocks, - ); + this.renameVariableWithConflict(variable, newName, conflictVar, blocks); } } finally { eventUtils.setGroup(existingGroup); @@ -120,10 +115,17 @@ export class VariableMap * Rename a variable by updating its name in the variable map. Identify the * variable to rename with the given ID. * + * @deprecated v12, use VariableMap.renameVariable. * @param id ID of the variable to rename. * @param newName New variable name. */ renameVariableById(id: string, newName: string) { + deprecation.warn( + 'VariableMap.renameVariableById', + 'v12', + 'v13', + 'VariableMap.renameVariable', + ); const variable = this.getVariableById(id); if (!variable) { throw Error("Tried to rename a variable that didn't exist. ID: " + id); @@ -140,7 +142,7 @@ export class VariableMap * @param newName New variable name. * @param blocks The list of all blocks in the workspace. */ - private renameVariableAndUses_( + private renameVariableAndUses( variable: IVariableModel, newName: string, blocks: Block[], @@ -165,7 +167,7 @@ export class VariableMap * @param conflictVar The variable that was already using newName. * @param blocks The list of all blocks in the workspace. */ - private renameVariableWithConflict_( + private renameVariableWithConflict( variable: IVariableModel, newName: string, conflictVar: IVariableModel, @@ -176,7 +178,7 @@ export class VariableMap if (newName !== oldCase) { // Simple rename to change the case and update references. - this.renameVariableAndUses_(conflictVar, newName, blocks); + this.renameVariableAndUses(conflictVar, newName, blocks); } // These blocks now refer to a different variable. @@ -295,9 +297,10 @@ export class VariableMap } /** - * @deprecated v12 - Delete a variables by the passed in ID and all of its - * uses from this workspace. May prompt the user for confirmation. + * Delete a variables by the passed in ID and all of its uses from this + * workspace. May prompt the user for confirmation. * + * @deprecated v12, use Blockly.Variables.deleteVariable. * @param id ID of variable to delete. */ deleteVariableById(id: string) { @@ -378,29 +381,6 @@ export class VariableMap return [...this.variableMap.keys()]; } - /** - * Return all variable and potential variable types. This list always - * contains the empty string. - * - * @param ws The workspace used to look for potential variables. This can be - * different than the workspace stored on this object if the passed in ws - * is a flyout workspace. - * @returns List of variable types. - * @internal - */ - getVariableTypes(ws: Workspace | null): string[] { - const variableTypes = new Set(this.variableMap.keys()); - if (ws && ws.getPotentialVariableMap()) { - for (const key of ws.getPotentialVariableMap()!.getTypes()) { - variableTypes.add(key); - } - } - if (!variableTypes.has('')) { - variableTypes.add(''); - } - return Array.from(variableTypes.values()); - } - /** * Return all variables of all types. * @@ -417,9 +397,16 @@ export class VariableMap /** * Returns all of the variable names of all types. * + * @deprecated v12, use Blockly.Variables.getAllVariables. * @returns All of the variable names of all types. */ getAllVariableNames(): string[] { + deprecation.warn( + 'VariableMap.getAllVariableNames', + 'v12', + 'v13', + 'Blockly.Variables.getAllVariables', + ); const names: string[] = []; for (const variables of this.variableMap.values()) { for (const variable of variables.values()) { @@ -430,8 +417,9 @@ export class VariableMap } /** - * @deprecated v12 - Find all the uses of a named variable. + * Find all the uses of a named variable. * + * @deprecated v12, use Blockly.Variables.getVariableUsesById. * @param id ID of the variable to find. * @returns Array of block usages. */ diff --git a/core/variable_model.ts b/core/variable_model.ts index f298480843b..5be8d54b352 100644 --- a/core/variable_model.ts +++ b/core/variable_model.ts @@ -140,21 +140,6 @@ export class VariableModel implements IVariableModel { workspace.getVariableMap().addVariable(variable); eventUtils.fire(new (eventUtils.get(eventUtils.VAR_CREATE))(variable)); } - - /** - * A custom compare function for the VariableModel objects. - * - * @param var1 First variable to compare. - * @param var2 Second variable to compare. - * @returns -1 if name of var1 is less than name of var2, 0 if equal, and 1 if - * greater. - * @internal - */ - static compareByName(var1: VariableModel, var2: VariableModel): number { - return var1 - .getName() - .localeCompare(var2.getName(), undefined, {sensitivity: 'base'}); - } } registry.register( diff --git a/core/workspace.ts b/core/workspace.ts index 2981fc6ada5..9e7d7c88432 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -25,6 +25,7 @@ import type {IConnectionChecker} from './interfaces/i_connection_checker.js'; import {Options} from './options.js'; import * as registry from './registry.js'; import * as arrayUtils from './utils/array.js'; +import * as deprecation from './utils/deprecation.js'; import * as idGenerator from './utils/idgenerator.js'; import * as math from './utils/math.js'; import type * as toolbox from './utils/toolbox.js'; @@ -381,13 +382,19 @@ export class Workspace implements IASTNodeLocation { /* Begin functions that are just pass-throughs to the variable map. */ /** - * Rename a variable by updating its name in the variable map. Identify the - * variable to rename with the given ID. + * @deprecated v12 - Rename a variable by updating its name in the variable + * map. Identify the variable to rename with the given ID. * * @param id ID of the variable to rename. * @param newName New variable name. */ renameVariableById(id: string, newName: string) { + deprecation.warn( + 'Blockly.Workspace.renameVariableById', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().renameVariable', + ); const variable = this.variableMap.getVariableById(id); if (!variable) return; this.variableMap.renameVariable(variable, newName); @@ -396,6 +403,7 @@ export class Workspace implements IASTNodeLocation { /** * Create a variable with a given name, optional type, and optional ID. * + * @deprecated v12, use Blockly.Workspace.getVariableMap().createVariable. * @param name The name of the variable. This must be unique across variables * and procedures. * @param opt_type The type of the variable like 'int' or 'string'. @@ -409,6 +417,12 @@ export class Workspace implements IASTNodeLocation { opt_type?: string | null, opt_id?: string | null, ): IVariableModel { + deprecation.warn( + 'Blockly.Workspace.createVariable', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().createVariable', + ); return this.variableMap.createVariable( name, opt_type ?? undefined, @@ -419,10 +433,17 @@ export class Workspace implements IASTNodeLocation { /** * Find all the uses of the given variable, which is identified by ID. * + * @deprecated v12, use Blockly.Workspace.getVariableMap().getVariableUsesById * @param id ID of the variable to find. * @returns Array of block usages. */ getVariableUsesById(id: string): Block[] { + deprecation.warn( + 'Blockly.Workspace.getVariableUsesById', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().getVariableUsesById', + ); return Variables.getVariableUsesById(this, id); } @@ -430,9 +451,16 @@ export class Workspace implements IASTNodeLocation { * Delete a variables by the passed in ID and all of its uses from this * workspace. May prompt the user for confirmation. * + * @deprecated v12, use Blockly.Workspace.getVariableMap().deleteVariable. * @param id ID of variable to delete. */ deleteVariableById(id: string) { + deprecation.warn( + 'Blockly.Workspace.deleteVariableById', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().deleteVariable', + ); const variable = this.variableMap.getVariableById(id); if (!variable) { console.warn(`Can't delete non-existent variable: ${id}`); @@ -445,6 +473,7 @@ export class Workspace implements IASTNodeLocation { * Find the variable by the given name and return it. Return null if not * found. * + * @deprecated v12, use Blockly.Workspace.getVariableMap().getVariable. * @param name The name to check for. * @param opt_type The type of the variable. If not provided it defaults to * the empty string, which is a specific type. @@ -454,6 +483,12 @@ export class Workspace implements IASTNodeLocation { name: string, opt_type?: string, ): IVariableModel | null { + deprecation.warn( + 'Blockly.Workspace.getVariable', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().getVariable', + ); // TODO (#1559): Possibly delete this function after resolving #1559. return this.variableMap.getVariable(name, opt_type); } @@ -461,10 +496,17 @@ export class Workspace implements IASTNodeLocation { /** * Find the variable by the given ID and return it. Return null if not found. * + * @deprecated v12, use Blockly.Workspace.getVariableMap().getVariableById. * @param id The ID to check for. * @returns The variable with the given ID. */ getVariableById(id: string): IVariableModel | null { + deprecation.warn( + 'Blockly.Workspace.getVariableById', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().getVariableById', + ); return this.variableMap.getVariableById(id); } @@ -472,44 +514,50 @@ export class Workspace implements IASTNodeLocation { * Find the variable with the specified type. If type is null, return list of * variables with empty string type. * + * @deprecated v12, use Blockly.Workspace.getVariableMap().getVariablesOfType. * @param type Type of the variables to find. * @returns The sought after variables of the passed in type. An empty array * if none are found. */ getVariablesOfType(type: string | null): IVariableModel[] { - return this.variableMap.getVariablesOfType(type ?? ''); - } - - /** - * Return all variable types. - * - * @returns List of variable types. - * @internal - */ - getVariableTypes(): string[] { - const variableTypes = new Set(this.variableMap.getTypes()); - (this.potentialVariableMap?.getTypes() ?? []).forEach((t) => - variableTypes.add(t), + deprecation.warn( + 'Blockly.Workspace.getVariablesOfType', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().getVariablesOfType', ); - variableTypes.add(''); - return Array.from(variableTypes.values()); + return this.variableMap.getVariablesOfType(type ?? ''); } /** * Return all variables of all types. * + * @deprecated v12, use Blockly.Workspace.getVariableMap().getAllVariables. * @returns List of variable models. */ getAllVariables(): IVariableModel[] { + deprecation.warn( + 'Blockly.Workspace.getAllVariables', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().getAllVariables', + ); return this.variableMap.getAllVariables(); } /** * Returns all variable names of all types. * + * @deprecated v12, use Blockly.Workspace.getVariableMap().getAllVariables. * @returns List of all variable names of all types. */ getAllVariableNames(): string[] { + deprecation.warn( + 'Blockly.Workspace.getAllVariableNames', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().getAllVariables', + ); return this.variableMap.getAllVariables().map((v) => v.getName()); } /* End functions that are just pass-throughs to the variable map. */ diff --git a/tests/mocha/field_variable_test.js b/tests/mocha/field_variable_test.js index 63dd644c393..fa332957bab 100644 --- a/tests/mocha/field_variable_test.js +++ b/tests/mocha/field_variable_test.js @@ -388,8 +388,7 @@ suite('Variable Fields', function () { fieldVariable.variableTypes = null; const resultTypes = fieldVariable.getVariableTypes(); - // The empty string is always one of the options. - assert.deepEqual(resultTypes, ['type1', 'type2', '']); + assert.deepEqual(resultTypes, ['type1', 'type2']); }); test('variableTypes is the empty list', function () { const fieldVariable = new Blockly.FieldVariable('name1'); diff --git a/tests/mocha/variable_map_test.js b/tests/mocha/variable_map_test.js index 76c1702cc71..8b60093fc16 100644 --- a/tests/mocha/variable_map_test.js +++ b/tests/mocha/variable_map_test.js @@ -187,24 +187,6 @@ suite('Variable Map', function () { }); }); - suite('getVariableTypes', function () { - test('Trivial', function () { - this.variableMap.createVariable('name1', 'type1', 'id1'); - this.variableMap.createVariable('name2', 'type1', 'id2'); - this.variableMap.createVariable('name3', 'type2', 'id3'); - this.variableMap.createVariable('name4', 'type3', 'id4'); - const resultArray = this.variableMap.getVariableTypes(); - // The empty string is always an option. - assert.deepEqual(resultArray, ['type1', 'type2', 'type3', '']); - }); - - test('None', function () { - // The empty string is always an option. - const resultArray = this.variableMap.getVariableTypes(); - assert.deepEqual(resultArray, ['']); - }); - }); - suite('getVariablesOfType', function () { test('Trivial', function () { const var1 = this.variableMap.createVariable('name1', 'type1', 'id1'); From 2619fb803c185a1ce61715d3ec8b15ae9264c613 Mon Sep 17 00:00:00 2001 From: dianaprahoveanu23 <142212685+dianaprahoveanu23@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:33:59 +0100 Subject: [PATCH 033/222] feat: Add a blocklyNotEditable CSS class to the block's root SVG (#8391) * feat: added blockyNotEditable CSS class to the block's root SVG * Run linter to fix code style issues --- core/block_svg.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/block_svg.ts b/core/block_svg.ts index 5a059e47b8d..a289db55685 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -732,6 +732,13 @@ export class BlockSvg */ override setEditable(editable: boolean) { super.setEditable(editable); + + if (editable) { + dom.removeClass(this.svgGroup_, 'blocklyNotEditable'); + } else { + dom.addClass(this.svgGroup_, 'blocklyNotEditable'); + } + const icons = this.getIcons(); for (let i = 0; i < icons.length; i++) { icons[i].updateEditable(); From 5d825f0a60f6009cc2f55dfb5ea14b6b151d4c8c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 25 Jul 2024 10:07:48 -0700 Subject: [PATCH 034/222] chore: Removed @internal annotation from public Field methods. (#8426) * chore: Removed @internal annotation from public Field methods. * chore: make forceRerender non-internal. --- core/field.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/core/field.ts b/core/field.ts index 68f4e2bb420..229c3626daf 100644 --- a/core/field.ts +++ b/core/field.ts @@ -408,7 +408,6 @@ export abstract class Field * called by Blockly.Xml. * * @param fieldElement The element containing info about the field's state. - * @internal */ fromXml(fieldElement: Element) { // Any because gremlins live here. No touchie! @@ -421,7 +420,6 @@ export abstract class Field * @param fieldElement The element to populate with info about the field's * state. * @returns The element containing info about the field's state. - * @internal */ toXml(fieldElement: Element): Element { // Any because gremlins live here. No touchie! @@ -440,7 +438,6 @@ export abstract class Field * {@link https://developers.devsite.google.com/blockly/guides/create-custom-blocks/fields/customizing-fields/creating#full_serialization_and_backing_data | field serialization docs} * for more information. * @returns JSON serializable state. - * @internal */ saveState(_doFullSerialization?: boolean): AnyDuringMigration { const legacyState = this.saveLegacyState(Field); @@ -455,7 +452,6 @@ export abstract class Field * called by the serialization system. * * @param state The state we want to apply to the field. - * @internal */ loadState(state: AnyDuringMigration) { if (this.loadLegacyState(Field, state)) { @@ -518,8 +514,6 @@ export abstract class Field /** * Dispose of all DOM objects and events belonging to this editable field. - * - * @internal */ dispose() { dropDownDiv.hideIfOwner(this); @@ -1054,8 +1048,6 @@ export abstract class Field * rerender this field and adjust for any sizing changes. * Other fields on the same block will not rerender, because their sizes have * already been recorded. - * - * @internal */ forceRerender() { this.isDirty_ = true; @@ -1303,7 +1295,6 @@ export abstract class Field * Subclasses may override this. * * @returns True if this field has any variable references. - * @internal */ referencesVariables(): boolean { return false; @@ -1312,8 +1303,6 @@ export abstract class Field /** * Refresh the variable name referenced by this field if this field references * variables. - * - * @internal */ refreshVariableName() {} // NOP From af0a724b3e0294d05608876f43caebbfefb2b5f5 Mon Sep 17 00:00:00 2001 From: Skye <81345074+Skye967@users.noreply.github.com> Date: Fri, 26 Jul 2024 19:16:22 -0600 Subject: [PATCH 035/222] fix: use `:focus` pseudo class instead of `blocklyFocused` (#8360) * bug: removed blocklyFocused from menu.ts and dropdown.ts, changed css style to :focus * removed blocklyFocused from menu.ts * resubmit * core css removed blocklyFocused * fix core css * menu file import cleanup, linting error --- core/css.ts | 5 ++--- core/dropdowndiv.ts | 6 ------ core/menu.ts | 3 --- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/core/css.ts b/core/css.ts index 20c5730935e..7a00536045d 100644 --- a/core/css.ts +++ b/core/css.ts @@ -5,7 +5,6 @@ */ // Former goog.module ID: Blockly.Css - /** Has CSS already been injected? */ let injected = false; @@ -119,7 +118,7 @@ let content = ` box-shadow: 0 0 3px 1px rgba(0,0,0,.3); } -.blocklyDropDownDiv.blocklyFocused { +.blocklyDropDownDiv:focus { box-shadow: 0 0 6px 1px rgba(0,0,0,.3); } @@ -445,7 +444,7 @@ input[type=number] { z-index: 20000; /* Arbitrary, but some apps depend on it... */ } -.blocklyWidgetDiv .blocklyMenu.blocklyFocused { +.blocklyWidgetDiv .blocklyMenu:focus { box-shadow: 0 0 6px 1px rgba(0,0,0,.3); } diff --git a/core/dropdowndiv.ts b/core/dropdowndiv.ts index c90661c4ea7..35eb6eaed19 100644 --- a/core/dropdowndiv.ts +++ b/core/dropdowndiv.ts @@ -136,12 +136,6 @@ export function createDom() { // Handle focusin/out events to add a visual indicator when // a child is focused or blurred. - div.addEventListener('focusin', function () { - dom.addClass(div, 'blocklyFocused'); - }); - div.addEventListener('focusout', function () { - dom.removeClass(div, 'blocklyFocused'); - }); } /** diff --git a/core/menu.ts b/core/menu.ts index 29615925bc9..31eda5c3d6c 100644 --- a/core/menu.ts +++ b/core/menu.ts @@ -15,7 +15,6 @@ import * as browserEvents from './browser_events.js'; import type {MenuItem} from './menuitem.js'; import * as aria from './utils/aria.js'; import {Coordinate} from './utils/coordinate.js'; -import * as dom from './utils/dom.js'; import type {Size} from './utils/size.js'; import * as style from './utils/style.js'; @@ -156,7 +155,6 @@ export class Menu { const el = this.getElement(); if (el) { el.focus({preventScroll: true}); - dom.addClass(el, 'blocklyFocused'); } } @@ -165,7 +163,6 @@ export class Menu { const el = this.getElement(); if (el) { el.blur(); - dom.removeClass(el, 'blocklyFocused'); } } From 82c7aad4e7382f4cabfaed1000bd78725a665f07 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 29 Jul 2024 12:00:52 -0700 Subject: [PATCH 036/222] feat: Add a VarTypeChange event. (#8402) * feat: Add a VarTypeChange event. * chore: Update copyright date. * refactor: Inline fields in the constructor. --- core/events/events.ts | 4 + core/events/events_var_type_change.ts | 122 ++++++++++++++++++++++ core/events/utils.ts | 5 + tests/mocha/event_var_type_change_test.js | 43 ++++++++ tests/mocha/index.html | 1 + 5 files changed, 175 insertions(+) create mode 100644 core/events/events_var_type_change.ts create mode 100644 tests/mocha/event_var_type_change_test.js diff --git a/core/events/events.ts b/core/events/events.ts index b31cf7dc788..67c78203fe0 100644 --- a/core/events/events.ts +++ b/core/events/events.ts @@ -43,6 +43,7 @@ import {VarBase, VarBaseJson} from './events_var_base.js'; import {VarCreate, VarCreateJson} from './events_var_create.js'; import {VarDelete, VarDeleteJson} from './events_var_delete.js'; import {VarRename, VarRenameJson} from './events_var_rename.js'; +import {VarTypeChange, VarTypeChangeJson} from './events_var_type_change.js'; import {ViewportChange, ViewportChangeJson} from './events_viewport.js'; import * as eventUtils from './utils.js'; import {FinishedLoading} from './workspace_events.js'; @@ -105,6 +106,8 @@ export {VarDelete}; export {VarDeleteJson}; export {VarRename}; export {VarRenameJson}; +export {VarTypeChange}; +export {VarTypeChangeJson}; export {ViewportChange}; export {ViewportChangeJson}; @@ -140,6 +143,7 @@ export const UI = eventUtils.UI; export const VAR_CREATE = eventUtils.VAR_CREATE; export const VAR_DELETE = eventUtils.VAR_DELETE; export const VAR_RENAME = eventUtils.VAR_RENAME; +export const VAR_TYPE_CHAGE = eventUtils.VAR_TYPE_CHANGE; export const VIEWPORT_CHANGE = eventUtils.VIEWPORT_CHANGE; // Event utils. diff --git a/core/events/events_var_type_change.ts b/core/events/events_var_type_change.ts new file mode 100644 index 00000000000..ab86866203c --- /dev/null +++ b/core/events/events_var_type_change.ts @@ -0,0 +1,122 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for a variable type change event. + * + * @class + */ + +import * as registry from '../registry.js'; +import type { + IVariableModel, + IVariableState, +} from '../interfaces/i_variable_model.js'; + +import {VarBase, VarBaseJson} from './events_var_base.js'; +import * as eventUtils from './utils.js'; +import type {Workspace} from '../workspace.js'; + +/** + * Notifies listeners that a variable's type has changed. + */ +export class VarTypeChange extends VarBase { + override type = eventUtils.VAR_TYPE_CHANGE; + + /** + * @param variable The variable whose type changed. Undefined for a blank event. + * @param oldType The old type of the variable. Undefined for a blank event. + * @param newType The new type of the variable. Undefined for a blank event. + */ + constructor( + variable?: IVariableModel, + public oldType?: string, + public newType?: string, + ) { + super(variable); + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): VarTypeChangeJson { + const json = super.toJson() as VarTypeChangeJson; + if (!this.oldType || !this.newType) { + throw new Error( + "The variable's types are undefined. Either pass them to " + + 'the constructor, or call fromJson', + ); + } + json['oldType'] = this.oldType; + json['newType'] = this.newType; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of VarTypeChange, but we can't specify that due to the fact that + * parameters to static methods in subclasses must be supertypes of + * parameters to static methods in superclasses. + * @internal + */ + static fromJson( + json: VarTypeChangeJson, + workspace: Workspace, + event?: any, + ): VarTypeChange { + const newEvent = super.fromJson( + json, + workspace, + event ?? new VarTypeChange(), + ) as VarTypeChange; + newEvent.oldType = json['oldType']; + newEvent.newType = json['newType']; + return newEvent; + } + + /** + * Run a variable type change event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.varId) { + throw new Error( + 'The var ID is undefined. Either pass a variable to ' + + 'the constructor, or call fromJson', + ); + } + if (!this.oldType || !this.newType) { + throw new Error( + "The variable's types are undefined. Either pass them to " + + 'the constructor, or call fromJson', + ); + } + const variable = workspace.getVariableMap().getVariableById(this.varId); + if (!variable) return; + if (forward) { + workspace.getVariableMap().changeVariableType(variable, this.newType); + } else { + workspace.getVariableMap().changeVariableType(variable, this.oldType); + } + } +} + +export interface VarTypeChangeJson extends VarBaseJson { + oldType: string; + newType: string; +} + +registry.register( + registry.Type.EVENT, + eventUtils.VAR_TYPE_CHANGE, + VarTypeChange, +); diff --git a/core/events/utils.ts b/core/events/utils.ts index 2d434594b19..dc05b632ee5 100644 --- a/core/events/utils.ts +++ b/core/events/utils.ts @@ -111,6 +111,11 @@ export const VAR_DELETE = 'var_delete'; */ export const VAR_RENAME = 'var_rename'; +/** + * Name of event that changes a variable's type. + */ +export const VAR_TYPE_CHANGE = 'var_type_change'; + /** * Name of generic event that records a UI change. */ diff --git a/tests/mocha/event_var_type_change_test.js b/tests/mocha/event_var_type_change_test.js new file mode 100644 index 00000000000..d19b0421a33 --- /dev/null +++ b/tests/mocha/event_var_type_change_test.js @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {assert} from '../../node_modules/chai/chai.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Var Type Change Event', function () { + setup(function () { + sharedTestSetup.call(this); + this.workspace = new Blockly.Workspace(); + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + suite('Serialization', function () { + test('variable type change events round-trip through JSON', function () { + const varModel = new Blockly.VariableModel( + this.workspace, + 'name', + 'foo', + 'id', + ); + const origEvent = new Blockly.Events.VarTypeChange( + varModel, + 'foo', + 'bar', + ); + + const json = origEvent.toJson(); + const newEvent = new Blockly.Events.fromJson(json, this.workspace); + + assert.deepEqual(newEvent, origEvent); + }); + }); +}); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index ff3467907d7..58a71e0acdc 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -76,6 +76,7 @@ import './event_var_create_test.js'; import './event_var_delete_test.js'; import './event_var_rename_test.js'; + import './event_var_type_change_test.js'; import './event_viewport_test.js'; import './extensions_test.js'; import './field_checkbox_test.js'; From 4b95cb77af7f79ee2b393b30edbf52ff21a5a623 Mon Sep 17 00:00:00 2001 From: Bhargav <143892094+vexora-0@users.noreply.github.com> Date: Tue, 30 Jul 2024 08:01:37 +0530 Subject: [PATCH 037/222] feat: Added blocklyImageField CSS class to image fields https://github.com/google/blockly/issues/8314 (#8439) --- core/field_image.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/field_image.ts b/core/field_image.ts index 6e83e3405c6..650575f59a3 100644 --- a/core/field_image.ts +++ b/core/field_image.ts @@ -151,6 +151,10 @@ export class FieldImage extends Field { this.value_ as string, ); + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyImageField'); + } + if (this.clickHandler) { this.imageElement.style.cursor = 'pointer'; } From dc1f276759e55ba3d36fe2eb675e71c25159e277 Mon Sep 17 00:00:00 2001 From: dakshkanaujia <58256644+dakshkanaujia@users.noreply.github.com> Date: Tue, 30 Jul 2024 20:54:15 +0530 Subject: [PATCH 038/222] fix!: Redundant blockly non selectable #8328 (#8433) * Remove redundant blocklyNonSelectable class and integrate non-selectability into existing classes * Removed .gitpod file * fix: remove redundant blocklyNonSelectable class and integrate non-selectability into existing classes https://github.com/google/blockly/issues/8328 * fix: remove redundant blocklyNonSelectable class and integrate non-selectability into existing classes #8328 * fix: remove redundant blocklyNonSelectable class and integrate non-selectability into existing classes #8328 * fix: remove redundant blocklyNonSelectable class and integrate non-selectability into existing classes #8328 * fix: remove redundant blocklyNonSelectable class and integrate non-selectability into existing classes #8328 * fix: remove redundant blocklyNonSelectable class and integrate non-selectability into existing classes --- core/css.ts | 12 ++++++------ core/menu.ts | 3 ++- core/toolbox/toolbox.ts | 4 +++- tests/mocha/toolbox_test.js | 5 +---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/css.ts b/core/css.ts index 7a00536045d..00d4f1be462 100644 --- a/core/css.ts +++ b/core/css.ts @@ -80,12 +80,6 @@ let content = ` touch-action: none; } -.blocklyNonSelectable { - user-select: none; - -ms-user-select: none; - -webkit-user-select: none; -} - .blocklyBlockCanvas.blocklyCanvasTransitioning, .blocklyBubbleCanvas.blocklyCanvasTransitioning { transition: transform .5s; @@ -430,6 +424,9 @@ input[type=number] { } .blocklyWidgetDiv .blocklyMenu { + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; background: #fff; border: 1px solid transparent; box-shadow: 0 0 3px 1px rgba(0,0,0,.3); @@ -449,6 +446,9 @@ input[type=number] { } .blocklyDropDownDiv .blocklyMenu { + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; background: inherit; /* Compatibility with gapi, reset from goog-menu */ border: inherit; /* Compatibility with gapi, reset from goog-menu */ font: normal 13px "Helvetica Neue", Helvetica, sans-serif; diff --git a/core/menu.ts b/core/menu.ts index 31eda5c3d6c..6085d927424 100644 --- a/core/menu.ts +++ b/core/menu.ts @@ -82,9 +82,10 @@ export class Menu { * @param container Element upon which to append this menu. * @returns The menu's root DOM element. */ + render(container: Element): HTMLDivElement { const element = document.createElement('div'); - element.className = 'blocklyMenu blocklyNonSelectable'; + element.className = 'blocklyMenu'; element.tabIndex = 0; if (this.roleName) { aria.setRole(element, this.roleName); diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 1e2a5970f61..cd91b2d8ae8 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -199,7 +199,6 @@ export class Toolbox const toolboxContainer = document.createElement('div'); toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v'); dom.addClass(toolboxContainer, 'blocklyToolboxDiv'); - dom.addClass(toolboxContainer, 'blocklyNonSelectable'); toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); return toolboxContainer; } @@ -1104,6 +1103,9 @@ Css.register(` /* Category tree in Toolbox. */ .blocklyToolboxDiv { + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; background-color: #ddd; overflow-x: visible; overflow-y: auto; diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js index b3cd45090dc..755f08cf8f2 100644 --- a/tests/mocha/toolbox_test.js +++ b/tests/mocha/toolbox_test.js @@ -47,10 +47,7 @@ suite('Toolbox', function () { test('Init called -> HtmlDiv is inserted before parent node', function () { const toolboxDiv = Blockly.common.getMainWorkspace().getInjectionDiv() .childNodes[0]; - assert.equal( - toolboxDiv.className, - 'blocklyToolboxDiv blocklyNonSelectable', - ); + assert.equal(toolboxDiv.className, 'blocklyToolboxDiv'); }); test('Init called -> Toolbox is subscribed to background and foreground colour', function () { const themeManager = this.toolbox.workspace_.getThemeManager(); From 9c88970d463b851820455d85de7b74e73d2209c4 Mon Sep 17 00:00:00 2001 From: Shreshtha Sharma <145495563+Shreshthaaa@users.noreply.github.com> Date: Wed, 31 Jul 2024 05:20:38 +0530 Subject: [PATCH 039/222] feat: added blocklyNotDetetable class to block_svg (#8406) * feat: added blocklynotdetetable class to block_svg * feat: added blocklynotdetetable class to block_svg --- core/block_svg.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/block_svg.ts b/core/block_svg.ts index a289db55685..7bdbd5b79d5 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -1079,6 +1079,20 @@ export class BlockSvg } } + /** + * Add blocklyNotDeletable class when block is not deletable + * Or remove class when block is deletable + */ + override setDeletable(deletable: boolean) { + super.setDeletable(deletable); + + if (deletable) { + dom.removeClass(this.svgGroup_, 'blocklyNotDeletable'); + } else { + dom.addClass(this.svgGroup_, 'blocklyNotDeletable'); + } + } + /** * Set whether the block is highlighted or not. Block highlighting is * often used to visually mark blocks currently being executed. From 203e422977a39efca29ed1fb00c74d6eaa3f973d Mon Sep 17 00:00:00 2001 From: Tejas Ghatule <141946130+CodeMaverick2@users.noreply.github.com> Date: Wed, 31 Jul 2024 06:42:48 +0530 Subject: [PATCH 040/222] feat: add the block's type as a CSS class to the block's root SVG (#8428) * feat: Added the block's type as a CSS class to the block's root SVG https://github.com/google/blockly/issues/8268 * fix: Added the block type as a CSS class to the blocks root SVG https://github.com/google/blockly/issues/8268 --- core/block_svg.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/block_svg.ts b/core/block_svg.ts index 7bdbd5b79d5..acead527a98 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -184,6 +184,9 @@ export class BlockSvg this.workspace = workspace; this.svgGroup_ = dom.createSvgElement(Svg.G, {}); + if (prototypeName) { + dom.addClass(this.svgGroup_, prototypeName); + } /** A block style object. */ this.style = workspace.getRenderer().getConstants().getBlockStyle(null); From 6393ab39ce2c9aaafd01c3b7247d1f675a30e642 Mon Sep 17 00:00:00 2001 From: surajguduru <140954256+surajguduru@users.noreply.github.com> Date: Wed, 31 Jul 2024 06:56:17 +0530 Subject: [PATCH 041/222] feat: add blocklyLabelField CSS class to label fields (#8423) --- core/field_label.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/field_label.ts b/core/field_label.ts index 2b77b0d25ff..0a73d0fb2dc 100644 --- a/core/field_label.ts +++ b/core/field_label.ts @@ -74,6 +74,9 @@ export class FieldLabel extends Field { if (this.class) { dom.addClass(this.getTextElement(), this.class); } + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyLabelField'); + } } /** From 17db6039b5ec1230741746b1df14e0225c7252d2 Mon Sep 17 00:00:00 2001 From: UtkershBasnet <119008923+UtkershBasnet@users.noreply.github.com> Date: Thu, 1 Aug 2024 04:03:25 +0530 Subject: [PATCH 042/222] fix!: Rename blocklyTreeIconOpen to blocklyToolboxCategoryIconOpen (#8440) --- core/toolbox/category.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/toolbox/category.ts b/core/toolbox/category.ts index b47ba657cff..653d848ec1b 100644 --- a/core/toolbox/category.ts +++ b/core/toolbox/category.ts @@ -138,7 +138,7 @@ export class ToolboxCategory 'label': 'blocklyTreeLabel', 'contents': 'blocklyToolboxCategoryGroup', 'selected': 'blocklyTreeSelected', - 'openicon': 'blocklyTreeIconOpen', + 'openicon': 'blocklyToolboxCategoryIconOpen', 'closedicon': 'blocklyTreeIconClosed', }; } @@ -708,11 +708,11 @@ Css.register(` background-position: 0 -17px; } -.blocklyTreeIconOpen { +.blocklyToolboxCategoryIconOpen { background-position: -16px -1px; } -.blocklyTreeSelected>.blocklyTreeIconOpen { +.blocklyTreeSelected>.blocklyToolboxCategoryIconOpen { background-position: -16px -17px; } From 8a1b01568ef6cb4f181c49d8e958d60f82c85c06 Mon Sep 17 00:00:00 2001 From: Aayush Khopade <145590889+Apocalypse96@users.noreply.github.com> Date: Thu, 1 Aug 2024 04:04:14 +0530 Subject: [PATCH 043/222] feat: Add a blocklyNumberField CSS class to number fields (#8414) * feat: Add a blocklyNumberField CSS class to number fields https://github.com/google/blockly/issues/8313 * feat: add 'blocklyNumberField' CSS class to FieldNumber Fixes https://github.com/google/blockly/issues/8313 --- core/field_number.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/field_number.ts b/core/field_number.ts index e8e51d06007..5aaf94c4cf5 100644 --- a/core/field_number.ts +++ b/core/field_number.ts @@ -19,6 +19,7 @@ import { FieldInputValidator, } from './field_input.js'; import * as aria from './utils/aria.js'; +import * as dom from './utils/dom.js'; /** * Class for an editable number field. @@ -307,6 +308,19 @@ export class FieldNumber extends FieldInput { return htmlInput; } + /** + * Initialize the field's DOM. + * + * @override + */ + + public override initView() { + super.initView(); + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyNumberField'); + } + } + /** * Construct a FieldNumber from a JSON arg object. * From 6887940e22262d7d571fd5df2546e4bf0cb244b3 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 2 Aug 2024 10:57:15 -0700 Subject: [PATCH 044/222] feat: add a method for subclasses of FieldVariable to get the default type. (#8453) --- core/field_variable.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/field_variable.ts b/core/field_variable.ts index 9dbba4ca3c7..23ea7c15a42 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -322,6 +322,15 @@ export class FieldVariable extends FieldDropdown { return this.variable; } + /** + * Gets the type of this field's default variable. + * + * @returns The default type for this variable field. + */ + protected getDefaultType(): string { + return this.defaultType; + } + /** * Gets the validation function for this field, or null if not set. * Returns null if the variable is not set, because validators should not From f10c3b0ee8f579f92b3faffffac1fe42a9f8de45 Mon Sep 17 00:00:00 2001 From: omwagh28 <151948718+omwagh28@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:56:05 +0530 Subject: [PATCH 045/222] fix!: Renamed the blocklyTreeSelected CSS class to blocklyToolboxSelected https://github.com/google/blockly/issues/8351 (#8459) --- core/toolbox/category.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/toolbox/category.ts b/core/toolbox/category.ts index 653d848ec1b..06f219e5eb8 100644 --- a/core/toolbox/category.ts +++ b/core/toolbox/category.ts @@ -137,7 +137,7 @@ export class ToolboxCategory 'icon': 'blocklyToolboxCategoryIcon', 'label': 'blocklyTreeLabel', 'contents': 'blocklyToolboxCategoryGroup', - 'selected': 'blocklyTreeSelected', + 'selected': 'blocklyToolboxSelected', 'openicon': 'blocklyToolboxCategoryIconOpen', 'closedicon': 'blocklyTreeIconClosed', }; @@ -659,7 +659,7 @@ export type CssConfig = ToolboxCategory.CssConfig; /** CSS for Toolbox. See css.js for use. */ Css.register(` -.blocklyToolboxCategory:not(.blocklyTreeSelected):hover { +.blocklyToolboxCategory:not(.blocklyToolboxSelected):hover { background-color: rgba(255, 255, 255, .2); } @@ -700,11 +700,11 @@ Css.register(` background-position: 0 -1px; } -.blocklyTreeSelected>.blocklyTreeIconClosed { +.blocklyToolboxSelected>.blocklyTreeIconClosed { background-position: -32px -17px; } -.blocklyToolboxDiv[dir="RTL"] .blocklyTreeSelected>.blocklyTreeIconClosed { +.blocklyToolboxDiv[dir="RTL"] .blocklyToolboxSelected>.blocklyTreeIconClosed { background-position: 0 -17px; } @@ -712,7 +712,7 @@ Css.register(` background-position: -16px -1px; } -.blocklyTreeSelected>.blocklyToolboxCategoryIconOpen { +.blocklyToolboxSelected>.blocklyToolboxCategoryIconOpen { background-position: -16px -17px; } @@ -727,7 +727,7 @@ Css.register(` cursor: url("<<>>/handdelete.cur"), auto; } -.blocklyTreeSelected .blocklyTreeLabel { +.blocklyToolboxSelected .blocklyTreeLabel { color: #fff; } `); From 9374c028d4461512419c72243d254402efcd164a Mon Sep 17 00:00:00 2001 From: Shreshtha Sharma <145495563+Shreshthaaa@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:05:35 +0530 Subject: [PATCH 046/222] feat: added block's style as a CSS class to block's root SVG (#8436) * fix: added block's style as a CSS class to block's root SVG * fix: added block's style as a CSS class to block's root SVG * fix: added block's style as a CSS class to block's root SVG --- core/block_svg.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index acead527a98..0da81f01db3 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -1178,7 +1178,10 @@ export class BlockSvg .getRenderer() .getConstants() .getBlockStyle(blockStyleName); - this.styleName_ = blockStyleName; + + if (this.styleName_) { + dom.removeClass(this.svgGroup_, this.styleName_); + } if (blockStyle) { this.hat = blockStyle.hat; @@ -1188,6 +1191,9 @@ export class BlockSvg this.style = blockStyle; this.applyColour(); + + dom.addClass(this.svgGroup_, blockStyleName); + this.styleName_ = blockStyleName; } else { throw Error('Invalid style name: ' + blockStyleName); } From 68dda116231f5dc347287a054098edfdaeba4de1 Mon Sep 17 00:00:00 2001 From: aishwaryavenkatesan <114367358+aishwaryavenkatesan@users.noreply.github.com> Date: Wed, 7 Aug 2024 12:07:34 -0400 Subject: [PATCH 047/222] fix!: deleted styles without associated classes from css.ts, issue #8285 (#8465) --- core/css.ts | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/core/css.ts b/core/css.ts index 00d4f1be462..5d1f0749964 100644 --- a/core/css.ts +++ b/core/css.ts @@ -134,18 +134,6 @@ let content = ` border-color: inherit; } -.blocklyDropDownButton { - display: inline-block; - float: left; - padding: 0; - margin: 4px; - border-radius: 4px; - outline: none; - border: 1px solid; - transition: box-shadow .1s; - cursor: pointer; -} - .blocklyArrowTop { border-top: 1px solid; border-left: 1px solid; @@ -160,21 +148,6 @@ let content = ` border-color: inherit; } -.blocklyResizeSE { - cursor: se-resize; - fill: #aaa; -} - -.blocklyResizeSW { - cursor: sw-resize; - fill: #aaa; -} - -.blocklyResizeLine { - stroke: #515A5A; - stroke-width: 1; -} - .blocklyHighlightedConnectionPath { fill: none; stroke: #fc3; @@ -270,10 +243,6 @@ let content = ` cursor: inherit; } -.blocklyHidden { - display: none; -} - .blocklyFieldDropdown:not(.blocklyHidden) { display: block; } From 59fab944f4ba61bb2e6e3685053ae467ccdb8056 Mon Sep 17 00:00:00 2001 From: Adityajaiswal03 <140907684+Adityajaiswal03@users.noreply.github.com> Date: Tue, 13 Aug 2024 01:40:38 +0530 Subject: [PATCH 048/222] feat: change blocklyEditableText to blocklyEditableField and blocklyNonEditableText to blocklyNonEditableField BREAKING CHANGE: The blocklyEditableText and blocklyNonEditableText identifiers have been renamed to blocklyEditableField and blocklyNonEditableField respectively. This change may require updates to any existing code that references the old identifiers. (#8475) --- core/css.ts | 2 +- core/field.ts | 8 ++++---- core/renderers/common/constants.ts | 10 +++++----- core/renderers/zelos/constants.ts | 16 ++++++++-------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/core/css.ts b/core/css.ts index 5d1f0749964..c7ae5539711 100644 --- a/core/css.ts +++ b/core/css.ts @@ -219,7 +219,7 @@ let content = ` font-family: monospace; } -.blocklyNonEditableText>text { +.blocklyNonEditableField>text { pointer-events: none; } diff --git a/core/field.ts b/core/field.ts index 229c3626daf..2d50c04eb5a 100644 --- a/core/field.ts +++ b/core/field.ts @@ -534,12 +534,12 @@ export abstract class Field return; } if (this.enabled_ && block.isEditable()) { - dom.addClass(group, 'blocklyEditableText'); - dom.removeClass(group, 'blocklyNonEditableText'); + dom.addClass(group, 'blocklyEditableField'); + dom.removeClass(group, 'blocklyNonEditableField'); group.style.cursor = this.CURSOR; } else { - dom.addClass(group, 'blocklyNonEditableText'); - dom.removeClass(group, 'blocklyEditableText'); + dom.addClass(group, 'blocklyNonEditableField'); + dom.removeClass(group, 'blocklyEditableField'); group.style.cursor = ''; } } diff --git a/core/renderers/common/constants.ts b/core/renderers/common/constants.ts index 568b7f444d5..c4ea9b24e5c 100644 --- a/core/renderers/common/constants.ts +++ b/core/renderers/common/constants.ts @@ -1132,14 +1132,14 @@ export class ConstantProvider { `${selector} .blocklyText {`, `fill: #fff;`, `}`, - `${selector} .blocklyNonEditableText>rect,`, - `${selector} .blocklyEditableText>rect {`, + `${selector} .blocklyNonEditableField>rect,`, + `${selector} .blocklyEditableField>rect {`, `fill: ${this.FIELD_BORDER_RECT_COLOUR};`, `fill-opacity: .6;`, `stroke: none;`, `}`, - `${selector} .blocklyNonEditableText>text,`, - `${selector} .blocklyEditableText>text {`, + `${selector} .blocklyNonEditableField>text,`, + `${selector} .blocklyEditableField>text {`, `fill: #000;`, `}`, @@ -1154,7 +1154,7 @@ export class ConstantProvider { `}`, // Editable field hover. - `${selector} .blocklyEditableText:not(.blocklyEditing):hover>rect {`, + `${selector} .blocklyEditableField:not(.blocklyEditing):hover>rect {`, `stroke: #fff;`, `stroke-width: 2;`, `}`, diff --git a/core/renderers/zelos/constants.ts b/core/renderers/zelos/constants.ts index 22e3f3782e9..28c2cb4fc6c 100644 --- a/core/renderers/zelos/constants.ts +++ b/core/renderers/zelos/constants.ts @@ -802,14 +802,14 @@ export class ConstantProvider extends BaseConstantProvider { `${selector} .blocklyText {`, `fill: #fff;`, `}`, - `${selector} .blocklyNonEditableText>rect:not(.blocklyDropdownRect),`, - `${selector} .blocklyEditableText>rect:not(.blocklyDropdownRect) {`, + `${selector} .blocklyNonEditableField>rect:not(.blocklyDropdownRect),`, + `${selector} .blocklyEditableField>rect:not(.blocklyDropdownRect) {`, `fill: ${this.FIELD_BORDER_RECT_COLOUR};`, `}`, - `${selector} .blocklyNonEditableText>text,`, - `${selector} .blocklyEditableText>text,`, - `${selector} .blocklyNonEditableText>g>text,`, - `${selector} .blocklyEditableText>g>text {`, + `${selector} .blocklyNonEditableField>text,`, + `${selector} .blocklyEditableField>text,`, + `${selector} .blocklyNonEditableField>g>text,`, + `${selector} .blocklyEditableField>g>text {`, `fill: #575E75;`, `}`, @@ -825,9 +825,9 @@ export class ConstantProvider extends BaseConstantProvider { // Editable field hover. `${selector} .blocklyDraggable:not(.blocklyDisabled)`, - ` .blocklyEditableText:not(.blocklyEditing):hover>rect,`, + ` .blocklyEditableField:not(.blocklyEditing):hover>rect,`, `${selector} .blocklyDraggable:not(.blocklyDisabled)`, - ` .blocklyEditableText:not(.blocklyEditing):hover>.blocklyPath {`, + ` .blocklyEditableField:not(.blocklyEditing):hover>.blocklyPath {`, `stroke: #fff;`, `stroke-width: 2;`, `}`, From 731fb40faa38ad47e23963b27c36f33daac58711 Mon Sep 17 00:00:00 2001 From: Jeremiah Saunders <46662314+UCYT5040@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:14:05 -0500 Subject: [PATCH 049/222] feat: implement `WorkspaceSvg` class manipulation (#8473) * Implement addClass and removeClass functions * feat: implement `WorkspaceSvg` class manipulation * Update core/workspace_svg.ts * Update core/workspace_svg.ts --- core/workspace_svg.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 55a7540bd85..910171007fc 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2432,6 +2432,28 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { // We could call scroll here, but that has extra checks we don't need to do. this.translate(x, y); } + + /** + * Adds a CSS class to the workspace. + * + * @param className Name of class to add. + */ + addClass(className: string) { + if (this.injectionDiv) { + dom.addClass(this.injectionDiv, className); + } + } + + /** + * Removes a CSS class from the workspace. + * + * @param className Name of class to remove. + */ + removeClass(className: string) { + if (this.injectionDiv) { + dom.removeClass(this.injectionDiv, className); + } + } } /** From 64fd9ad89a5d48ccc18708d322340ed1523fa753 Mon Sep 17 00:00:00 2001 From: Shreshtha Sharma <145495563+Shreshthaaa@users.noreply.github.com> Date: Wed, 14 Aug 2024 21:36:27 +0530 Subject: [PATCH 050/222] =?UTF-8?q?feat:=20added=20`blocklyHighlighted`=20?= =?UTF-8?q?CSS=20class=20to=20highlighted=20block's=20root=E2=80=A6=20(#84?= =?UTF-8?q?07)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: added 'blocklyHighlighted' CSS class to highlighted block's root svg * fix: added 'blocklyHighlighted' CSS class to highlighted block's root svg * fix: added 'blocklyHighlighted' CSS class to highlighted block's root svg * fix: added 'blocklyHighlighted' CSS class to highlighted block's root svg * fix: added 'blocklyHighlighted' CSS class to highlighted block's root svg --- core/renderers/common/path_object.ts | 3 +++ core/renderers/geras/path_object.ts | 6 +----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index c5ac5db20f0..12e23b6c4aa 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -170,14 +170,17 @@ export class PathObject implements IPathObject { * * @param enable True if highlighted. */ + updateHighlighted(enable: boolean) { if (enable) { this.svgPath.setAttribute( 'filter', 'url(#' + this.constants.embossFilterId + ')', ); + this.setClass_('blocklyHighlighted', true); } else { this.svgPath.setAttribute('filter', 'none'); + this.setClass_('blocklyHighlighted', false); } } diff --git a/core/renderers/geras/path_object.ts b/core/renderers/geras/path_object.ts index 6b058e5a752..321302a265a 100644 --- a/core/renderers/geras/path_object.ts +++ b/core/renderers/geras/path_object.ts @@ -103,14 +103,10 @@ export class PathObject extends BasePathObject { } override updateHighlighted(highlighted: boolean) { + super.updateHighlighted(highlighted); if (highlighted) { - this.svgPath.setAttribute( - 'filter', - 'url(#' + this.constants.embossFilterId + ')', - ); this.svgPathLight.style.display = 'none'; } else { - this.svgPath.setAttribute('filter', 'none'); this.svgPathLight.style.display = 'inline'; } } From 14d119b204d1d3cbad054f413e9de971ea9cc7d4 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 19 Aug 2024 15:47:00 -0700 Subject: [PATCH 051/222] fix: improve prompting when deleting variables (#8529) * fix: improve variable deletion behaviors. * fix: don't prompt about deletion of only 1 variable block when triggered programmatically. * fix: include the triggering block in the count of referencing blocks * fix: only count the triggering block as a referencing block if it's not in the flyout --- blocks/variables.ts | 9 +++++---- blocks/variables_dynamic.ts | 9 +++++---- core/field_variable.ts | 7 ++++++- core/flyout_button.ts | 2 +- core/variables.ts | 18 ++++++++++++++---- 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/blocks/variables.ts b/blocks/variables.ts index 8ac038fb2ce..987c5ab2fdc 100644 --- a/blocks/variables.ts +++ b/blocks/variables.ts @@ -165,11 +165,12 @@ const deleteOptionCallbackFactory = function ( block: VariableBlock, ): () => void { return function () { - const workspace = block.workspace; const variableField = block.getField('VAR') as FieldVariable; - const variable = variableField.getVariable()!; - workspace.deleteVariableById(variable.getId()); - (workspace as WorkspaceSvg).refreshToolboxSelection(); + const variable = variableField.getVariable(); + if (variable) { + Variables.deleteVariable(variable.getWorkspace(), variable, block); + } + (block.workspace as WorkspaceSvg).refreshToolboxSelection(); }; }; diff --git a/blocks/variables_dynamic.ts b/blocks/variables_dynamic.ts index ff94d8c96c4..9c1167a19af 100644 --- a/blocks/variables_dynamic.ts +++ b/blocks/variables_dynamic.ts @@ -176,11 +176,12 @@ const renameOptionCallbackFactory = function (block: VariableBlock) { */ const deleteOptionCallbackFactory = function (block: VariableBlock) { return function () { - const workspace = block.workspace; const variableField = block.getField('VAR') as FieldVariable; - const variable = variableField.getVariable()!; - workspace.deleteVariableById(variable.getId()); - (workspace as WorkspaceSvg).refreshToolboxSelection(); + const variable = variableField.getVariable(); + if (variable) { + Variables.deleteVariable(variable.getWorkspace(), variable, block); + } + (block.workspace as WorkspaceSvg).refreshToolboxSelection(); }; }; diff --git a/core/field_variable.ts b/core/field_variable.ts index 23ea7c15a42..042299dc293 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -27,6 +27,7 @@ import * as fieldRegistry from './field_registry.js'; import * as internalConstants from './internal_constants.js'; import type {Menu} from './menu.js'; import type {MenuItem} from './menuitem.js'; +import {WorkspaceSvg} from './workspace_svg.js'; import {Msg} from './msg.js'; import * as parsing from './utils/parsing.js'; import {Size} from './utils/size.js'; @@ -514,7 +515,11 @@ export class FieldVariable extends FieldDropdown { return; } else if (id === internalConstants.DELETE_VARIABLE_ID && this.variable) { // Delete variable. - this.sourceBlock_.workspace.deleteVariableById(this.variable.getId()); + const workspace = this.variable.getWorkspace(); + Variables.deleteVariable(workspace, this.variable, this.sourceBlock_); + if (workspace instanceof WorkspaceSvg) { + workspace.refreshToolboxSelection(); + } return; } } diff --git a/core/flyout_button.ts b/core/flyout_button.ts index e73403d77a0..dfc7b950747 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -179,7 +179,7 @@ export class FlyoutButton implements IASTNodeLocationSvg { fontWeight, fontFamily, ); - this.height = fontMetrics.height; + this.height = this.height || fontMetrics.height; if (!this.isFlyoutLabel) { this.width += 2 * FlyoutButton.TEXT_MARGIN_X; diff --git a/core/variables.ts b/core/variables.ts index 5da228f6cc2..bad87df0be4 100644 --- a/core/variables.ts +++ b/core/variables.ts @@ -714,15 +714,20 @@ export function getVariableUsesById(workspace: Workspace, id: string): Block[] { * * @param workspace The workspace from which to delete the variable. * @param variable The variable to delete. + * @param triggeringBlock The block from which this deletion was triggered, if + * any. Used to exclude it from checking and warning about blocks + * referencing the variable being deleted. */ export function deleteVariable( workspace: Workspace, variable: IVariableModel, + triggeringBlock?: Block, ) { // Check whether this variable is a function parameter before deleting. const variableName = variable.getName(); const uses = getVariableUsesById(workspace, variable.getId()); - for (let i = 0, block; (block = uses[i]); i++) { + for (let i = uses.length - 1; i >= 0; i--) { + const block = uses[i]; if ( block.type === 'procedures_defnoreturn' || block.type === 'procedures_defreturn' @@ -734,12 +739,15 @@ export function deleteVariable( dialog.alert(deleteText); return; } + if (block === triggeringBlock) { + uses.splice(i, 1); + } } - if (uses.length > 1) { + if ((triggeringBlock && uses.length) || uses.length > 1) { // Confirm before deleting multiple blocks. const confirmText = Msg['DELETE_VARIABLE_CONFIRMATION'] - .replace('%1', String(uses.length)) + .replace('%1', String(uses.length + (triggeringBlock ? 1 : 0))) .replace('%2', variableName); dialog.confirm(confirmText, (ok) => { if (ok && variable) { @@ -747,7 +755,9 @@ export function deleteVariable( } }); } else { - // No confirmation necessary for a single block. + // No confirmation necessary when the block that triggered the deletion is + // the only block referencing this variable or if only one block referencing + // this variable exists and the deletion was triggered programmatically. workspace.getVariableMap().deleteVariable(variable); } } From d6125d4fb94ac7f4ab9e57a2944a5aa1c6ead328 Mon Sep 17 00:00:00 2001 From: Arun Chandran <53257113+Arun-cn@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:31:07 +0530 Subject: [PATCH 052/222] fix!: Remove the blocklyMenuItemHighlight CSS class and use the hover (#8536) * fix!: Remove the blocklyMenuItemHighlight CSS class and use the hover * fix: Remove setHighlighted method in menuitem * fix: Remove blocklymenuitemhighlight css class --- core/css.ts | 3 +-- core/menu.ts | 2 -- core/menuitem.ts | 21 --------------------- 3 files changed, 1 insertion(+), 25 deletions(-) diff --git a/core/css.ts b/core/css.ts index c7ae5539711..d18d930a943 100644 --- a/core/css.ts +++ b/core/css.ts @@ -445,8 +445,7 @@ input[type=number] { cursor: inherit; } -/* State: hover. */ -.blocklyMenuItemHighlight { +.blocklyMenuItem:hover { background-color: rgba(0,0,0,.1); } diff --git a/core/menu.ts b/core/menu.ts index 6085d927424..f01c1edfb63 100644 --- a/core/menu.ts +++ b/core/menu.ts @@ -249,11 +249,9 @@ export class Menu { setHighlighted(item: MenuItem | null) { const currentHighlighted = this.highlightedItem; if (currentHighlighted) { - currentHighlighted.setHighlighted(false); this.highlightedItem = null; } if (item) { - item.setHighlighted(true); this.highlightedItem = item; // Bring the highlighted item into view. This has no effect if the menu is // not scrollable. diff --git a/core/menuitem.ts b/core/menuitem.ts index 7fff1a72bbc..3d5b28b709c 100644 --- a/core/menuitem.ts +++ b/core/menuitem.ts @@ -12,7 +12,6 @@ // Former goog.module ID: Blockly.MenuItem import * as aria from './utils/aria.js'; -import * as dom from './utils/dom.js'; import * as idGenerator from './utils/idgenerator.js'; /** @@ -68,7 +67,6 @@ export class MenuItem { 'blocklyMenuItem ' + (this.enabled ? '' : 'blocklyMenuItemDisabled ') + (this.checked ? 'blocklyMenuItemSelected ' : '') + - (this.highlight ? 'blocklyMenuItemHighlight ' : '') + (this.rightToLeft ? 'blocklyMenuItemRtl ' : ''); const content = document.createElement('div'); @@ -177,25 +175,6 @@ export class MenuItem { this.checked = checked; } - /** - * Highlights or unhighlights the component. - * - * @param highlight Whether to highlight or unhighlight the component. - * @internal - */ - setHighlighted(highlight: boolean) { - this.highlight = highlight; - const el = this.getElement(); - if (el && this.isEnabled()) { - const name = 'blocklyMenuItemHighlight'; - if (highlight) { - dom.addClass(el, name); - } else { - dom.removeClass(el, name); - } - } - } - /** * Returns true if the menu item is enabled, false otherwise. * From ba0762348d76f7e31c4d2144f295a2613a9273c1 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 21 Aug 2024 13:57:32 -0700 Subject: [PATCH 053/222] fix: display the correct variable reference count when deleting a variable. (#8549) --- core/variables.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/variables.ts b/core/variables.ts index bad87df0be4..8c06a8d911e 100644 --- a/core/variables.ts +++ b/core/variables.ts @@ -747,7 +747,13 @@ export function deleteVariable( if ((triggeringBlock && uses.length) || uses.length > 1) { // Confirm before deleting multiple blocks. const confirmText = Msg['DELETE_VARIABLE_CONFIRMATION'] - .replace('%1', String(uses.length + (triggeringBlock ? 1 : 0))) + .replace( + '%1', + String( + uses.length + + (triggeringBlock && !triggeringBlock.workspace.isFlyout ? 1 : 0), + ), + ) .replace('%2', variableName); dialog.confirm(confirmText, (ok) => { if (ok && variable) { From cb1c055bffdeb4a2a3298b4c6b58b941c442d4bc Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 3 Sep 2024 13:25:18 -0700 Subject: [PATCH 054/222] refactor: use getters for flyout width and height. (#8564) --- core/flyout_horizontal.ts | 10 +++++----- core/flyout_vertical.ts | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/flyout_horizontal.ts b/core/flyout_horizontal.ts index 6e77636e86b..c23dede74f7 100644 --- a/core/flyout_horizontal.ts +++ b/core/flyout_horizontal.ts @@ -98,7 +98,7 @@ export class HorizontalFlyout extends Flyout { if (atTop) { y = toolboxMetrics.height; } else { - y = viewMetrics.height - this.height_; + y = viewMetrics.height - this.getHeight(); } } else { if (atTop) { @@ -116,7 +116,7 @@ export class HorizontalFlyout extends Flyout { // to align the bottom edge of the flyout with the bottom edge of the // blocklyDiv, we calculate the full height of the div minus the height // of the flyout. - y = viewMetrics.height + absoluteMetrics.top - this.height_; + y = viewMetrics.height + absoluteMetrics.top - this.getHeight(); } } @@ -133,13 +133,13 @@ export class HorizontalFlyout extends Flyout { this.width_ = targetWorkspaceViewMetrics.width; const edgeWidth = targetWorkspaceViewMetrics.width - 2 * this.CORNER_RADIUS; - const edgeHeight = this.height_ - this.CORNER_RADIUS; + const edgeHeight = this.getHeight() - this.CORNER_RADIUS; this.setBackgroundPath(edgeWidth, edgeHeight); const x = this.getX(); const y = this.getY(); - this.positionAt_(this.width_, this.height_, x, y); + this.positionAt_(this.getWidth(), this.getHeight(), x, y); } /** @@ -380,7 +380,7 @@ export class HorizontalFlyout extends Flyout { flyoutHeight *= this.workspace_.scale; flyoutHeight += Scrollbar.scrollbarThickness; - if (this.height_ !== flyoutHeight) { + if (this.getHeight() !== flyoutHeight) { for (let i = 0, block; (block = blocks[i]); i++) { if (this.rectMap_.has(block)) { this.moveRectToBlock_(this.rectMap_.get(block)!, block); diff --git a/core/flyout_vertical.ts b/core/flyout_vertical.ts index 59682a390d2..374b0c33a54 100644 --- a/core/flyout_vertical.ts +++ b/core/flyout_vertical.ts @@ -86,7 +86,7 @@ export class VerticalFlyout extends Flyout { if (this.toolboxPosition_ === toolbox.Position.LEFT) { x = toolboxMetrics.width; } else { - x = viewMetrics.width - this.width_; + x = viewMetrics.width - this.getWidth(); } } else { if (this.toolboxPosition_ === toolbox.Position.LEFT) { @@ -104,7 +104,7 @@ export class VerticalFlyout extends Flyout { // to align the right edge of the flyout with the right edge of the // blocklyDiv, we calculate the full width of the div minus the width // of the flyout. - x = viewMetrics.width + absoluteMetrics.left - this.width_; + x = viewMetrics.width + absoluteMetrics.left - this.getWidth(); } } @@ -130,7 +130,7 @@ export class VerticalFlyout extends Flyout { const targetWorkspaceViewMetrics = metricsManager.getViewMetrics(); this.height_ = targetWorkspaceViewMetrics.height; - const edgeWidth = this.width_ - this.CORNER_RADIUS; + const edgeWidth = this.getWidth() - this.CORNER_RADIUS; const edgeHeight = targetWorkspaceViewMetrics.height - 2 * this.CORNER_RADIUS; this.setBackgroundPath(edgeWidth, edgeHeight); @@ -138,7 +138,7 @@ export class VerticalFlyout extends Flyout { const x = this.getX(); const y = this.getY(); - this.positionAt_(this.width_, this.height_, x, y); + this.positionAt_(this.getWidth(), this.getHeight(), x, y); } /** @@ -349,7 +349,7 @@ export class VerticalFlyout extends Flyout { flyoutWidth *= this.workspace_.scale; flyoutWidth += Scrollbar.scrollbarThickness; - if (this.width_ !== flyoutWidth) { + if (this.getWidth() !== flyoutWidth) { for (let i = 0, block; (block = blocks[i]); i++) { if (this.RTL) { // With the flyoutWidth known, right-align the blocks. From def80b3f31beb379a42fdeed620265c7fbbd8ab9 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 11 Sep 2024 12:37:32 -0700 Subject: [PATCH 055/222] fix: improve flyout performance (#8571) * fix: improve flyout performance * refactor: don't call position() in show() The later call to reflow() itself winds up calling position(), so this calculation is redundant. --- core/flyout_base.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 18f84480c5d..2ea85a0dfdd 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -402,7 +402,14 @@ export abstract class Flyout this.wheel_, ), ); - this.filterWrapper = this.filterForCapacity.bind(this); + this.filterWrapper = (event) => { + if ( + event.type === eventUtils.BLOCK_CREATE || + event.type === eventUtils.BLOCK_DELETE + ) { + this.filterForCapacity(); + } + }; this.targetWorkspace.addChangeListener(this.filterWrapper); // Dragging the flyout up and down. @@ -704,10 +711,17 @@ export abstract class Flyout this.filterForCapacity(); - // Correctly position the flyout's scrollbar when it opens. - this.position(); - - this.reflowWrapper = this.reflow.bind(this); + // Listen for block change events, and reflow the flyout in response. This + // accommodates e.g. resizing a non-autoclosing flyout in response to the + // user typing long strings into fields on the blocks in the flyout. + this.reflowWrapper = (event) => { + if ( + event.type === eventUtils.BLOCK_CHANGE || + event.type === eventUtils.BLOCK_FIELD_INTERMEDIATE_CHANGE + ) { + this.reflow(); + } + }; this.workspace_.addChangeListener(this.reflowWrapper); this.emptyRecycledBlocks(); } From 732bd7f6160ce9da4cc3063b3f2b47b3d204f446 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 13 Sep 2024 09:58:57 -0700 Subject: [PATCH 056/222] fix: size text with computed styles even when hidden (#8572) * fix: size text with computed styles even when hidden * refactor: remove unneeded try/catch. --- core/utils/dom.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/core/utils/dom.ts b/core/utils/dom.ts index e318e7e915e..87019dbb2de 100644 --- a/core/utils/dom.ts +++ b/core/utils/dom.ts @@ -208,16 +208,14 @@ export function getTextWidth(textElement: SVGTextElement): number { } } - // Attempt to compute fetch the width of the SVG text element. - try { - width = textElement.getComputedTextLength(); - } catch (e) { - // In other cases where we fail to get the computed text. Instead, use an - // approximation and do not cache the result. At some later point in time - // when the block is inserted into the visible DOM, this method will be - // called again and, at that point in time, will not throw an exception. - return textElement.textContent!.length * 8; - } + // Compute the width of the SVG text element. + const style = window.getComputedStyle(textElement); + width = getFastTextWidthWithSizeString( + textElement, + style.fontSize, + style.fontWeight, + style.fontFamily, + ); // Cache the computed width and return. if (cacheWidths) { From 476d454c05b1c7b72f7a9c3489ba452d11513d99 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 16 Sep 2024 09:14:56 -0700 Subject: [PATCH 057/222] fix: include potential variables in variable dropdowns in the flyout (#8574) --- core/field_variable.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/core/field_variable.ts b/core/field_variable.ts index 042299dc293..0c890f4d7bb 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -572,15 +572,23 @@ export class FieldVariable extends FieldDropdown { } const name = this.getText(); let variableModelList: IVariableModel[] = []; - if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) { + const sourceBlock = this.getSourceBlock(); + if (sourceBlock && !sourceBlock.isDeadOrDying()) { + const workspace = sourceBlock.workspace; const variableTypes = this.getVariableTypes(); // Get a copy of the list, so that adding rename and new variable options // doesn't modify the workspace's list. for (let i = 0; i < variableTypes.length; i++) { const variableType = variableTypes[i]; - const variables = - this.sourceBlock_.workspace.getVariablesOfType(variableType); + const variables = workspace.getVariablesOfType(variableType); variableModelList = variableModelList.concat(variables); + if (workspace.isFlyout) { + variableModelList = variableModelList.concat( + workspace + .getPotentialVariableMap() + ?.getVariablesOfType(variableType) ?? [], + ); + } } } variableModelList.sort(Variables.compareByName); From c79610cea6f7f1cdfad06772e0b1be8f0c17d6b6 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 18 Sep 2024 11:58:39 -0700 Subject: [PATCH 058/222] refactor: remove redundant flyout positioning. (#8573) * refactor: remove redundant flyout positioning. * fix: handle the case where there is a flyout without a toolbox --- core/workspace_svg.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 910171007fc..7c57f47cdbf 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -1049,8 +1049,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { resize() { if (this.toolbox_) { this.toolbox_.position(); - } - if (this.flyout) { + } else if (this.flyout) { this.flyout.position(); } From 6ec1bc5ba50960d2ceefcc0370b43e3f86f99fcc Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 25 Sep 2024 10:23:25 -0700 Subject: [PATCH 059/222] feat: Add the IFlyoutInflater interface. (#8581) * feat: Add the IFlyoutInflater interface. * fix: Add a return type for IFlyoutInflater.disposeElement(). * refactor: Add the gapForElement method. --- core/interfaces/i_flyout_inflater.ts | 41 ++++++++++++++++++++++++++++ core/registry.ts | 3 ++ 2 files changed, 44 insertions(+) create mode 100644 core/interfaces/i_flyout_inflater.ts diff --git a/core/interfaces/i_flyout_inflater.ts b/core/interfaces/i_flyout_inflater.ts new file mode 100644 index 00000000000..f4a3b6ee9a8 --- /dev/null +++ b/core/interfaces/i_flyout_inflater.ts @@ -0,0 +1,41 @@ +import type {IBoundedElement} from './i_bounded_element.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; + +export interface IFlyoutInflater { + /** + * Loads the object represented by the given state onto the workspace. + * + * Note that this method's interface is identical to that in ISerializer, to + * allow for code reuse. + * + * @param state A JSON representation of an element to inflate on the flyout. + * @param flyoutWorkspace The flyout's workspace, where the inflated element + * should be created. If the inflated element is an `IRenderedElement` it + * itself or the inflater should append it to the workspace; the flyout + * will not do so itself. The flyout is responsible for positioning the + * element, however. + * @returns The newly inflated flyout element. + */ + load(state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement; + + /** + * Returns the amount of spacing that should follow the element corresponding + * to the given JSON representation. + * + * @param state A JSON representation of the element preceding the gap. + * @param defaultGap The default gap for elements in this flyout. + * @returns The gap that should follow the given element. + */ + gapForElement(state: Object, defaultGap: number): number; + + /** + * Disposes of the given element. + * + * If the element in question resides on the flyout workspace, it should remove + * itself. Implementers are not otherwise required to fully dispose of the + * element; it may be e.g. cached for performance purposes. + * + * @param element The flyout element to dispose of. + */ + disposeElement(element: IBoundedElement): void; +} diff --git a/core/registry.ts b/core/registry.ts index c7e16e935e7..7d70fe2eb68 100644 --- a/core/registry.ts +++ b/core/registry.ts @@ -10,6 +10,7 @@ import type {Abstract} from './events/events_abstract.js'; import type {Field} from './field.js'; import type {IConnectionChecker} from './interfaces/i_connection_checker.js'; import type {IFlyout} from './interfaces/i_flyout.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; import type {IIcon} from './interfaces/i_icon.js'; import type {Input} from './inputs/input.js'; @@ -99,6 +100,8 @@ export class Type<_T> { 'flyoutsHorizontalToolbox', ); + static FLYOUT_INFLATER = new Type('flyoutInflater'); + static METRICS_MANAGER = new Type('metricsManager'); /** From 489aded31dcb352ae97acfeb3bb705e2da592e7a Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 27 Sep 2024 13:22:36 -0700 Subject: [PATCH 060/222] feat: Add inflaters for flyout labels and buttons. (#8593) * feat: Add inflaters for flyout labels and buttons. * chore: Temporarily re-add createDom(). * chore: fix JSDoc. * chore: Add license. * chore: Add TSDoc. --- core/button_flyout_inflater.ts | 63 +++++++++++++++++++++++++ core/flyout_button.ts | 85 ++++++++++++++++++++++++++-------- core/label_flyout_inflater.ts | 59 +++++++++++++++++++++++ 3 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 core/button_flyout_inflater.ts create mode 100644 core/label_flyout_inflater.ts diff --git a/core/button_flyout_inflater.ts b/core/button_flyout_inflater.ts new file mode 100644 index 00000000000..703dc606938 --- /dev/null +++ b/core/button_flyout_inflater.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; +import {FlyoutButton} from './flyout_button.js'; +import {ButtonOrLabelInfo} from './utils/toolbox.js'; +import * as registry from './registry.js'; + +/** + * Class responsible for creating buttons for flyouts. + */ +export class ButtonFlyoutInflater implements IFlyoutInflater { + /** + * Inflates a flyout button from the given state and adds it to the flyout. + * + * @param state A JSON representation of a flyout button. + * @param flyoutWorkspace The workspace to create the button on. + * @returns A newly created FlyoutButton. + */ + load(state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { + const button = new FlyoutButton( + flyoutWorkspace, + flyoutWorkspace.targetWorkspace!, + state as ButtonOrLabelInfo, + false, + ); + button.show(); + return button; + } + + /** + * Returns the amount of space that should follow this button. + * + * @param state A JSON representation of a flyout button. + * @param defaultGap The default spacing for flyout items. + * @returns The amount of space that should follow this button. + */ + gapForElement(state: Object, defaultGap: number): number { + return defaultGap; + } + + /** + * Disposes of the given button. + * + * @param element The flyout button to dispose of. + */ + disposeElement(element: IBoundedElement): void { + if (element instanceof FlyoutButton) { + element.dispose(); + } + } +} + +registry.register( + registry.Type.FLYOUT_INFLATER, + 'button', + ButtonFlyoutInflater, +); diff --git a/core/flyout_button.ts b/core/flyout_button.ts index dfc7b950747..41c3636fe89 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -20,12 +20,17 @@ import * as style from './utils/style.js'; import {Svg} from './utils/svg.js'; import type * as toolbox from './utils/toolbox.js'; import type {WorkspaceSvg} from './workspace_svg.js'; -import type {IASTNodeLocationSvg} from './blockly.js'; +import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {IRenderedElement} from './interfaces/i_rendered_element.js'; +import {Rect} from './utils/rect.js'; /** * Class for a button or label in the flyout. */ -export class FlyoutButton implements IASTNodeLocationSvg { +export class FlyoutButton + implements IASTNodeLocationSvg, IBoundedElement, IRenderedElement +{ /** The horizontal margin around the text in the button. */ static TEXT_MARGIN_X = 5; @@ -41,7 +46,8 @@ export class FlyoutButton implements IASTNodeLocationSvg { private readonly cssClass: string | null; /** Mouse up event data. */ - private onMouseUpWrapper: browserEvents.Data | null = null; + private onMouseDownWrapper: browserEvents.Data; + private onMouseUpWrapper: browserEvents.Data; info: toolbox.ButtonOrLabelInfo; /** The width of the button's rect. */ @@ -51,7 +57,7 @@ export class FlyoutButton implements IASTNodeLocationSvg { height = 0; /** The root SVG group for the button or label. */ - private svgGroup: SVGGElement | null = null; + private svgGroup: SVGGElement; /** The SVG element with the text of the label or button. */ private svgText: SVGTextElement | null = null; @@ -92,14 +98,6 @@ export class FlyoutButton implements IASTNodeLocationSvg { /** The JSON specifying the label / button. */ this.info = json; - } - - /** - * Create the button elements. - * - * @returns The button's SVG group. - */ - createDom(): SVGElement { let cssClass = this.isFlyoutLabel ? 'blocklyFlyoutLabel' : 'blocklyFlyoutButton'; @@ -198,15 +196,24 @@ export class FlyoutButton implements IASTNodeLocationSvg { this.updateTransform(); - // AnyDuringMigration because: Argument of type 'SVGGElement | null' is not - // assignable to parameter of type 'EventTarget'. + this.onMouseDownWrapper = browserEvents.conditionalBind( + this.svgGroup, + 'pointerdown', + this, + this.onMouseDown, + ); this.onMouseUpWrapper = browserEvents.conditionalBind( - this.svgGroup as AnyDuringMigration, + this.svgGroup, 'pointerup', this, this.onMouseUp, ); - return this.svgGroup!; + } + + createDom(): SVGElement { + // No-op, now handled in constructor. Will be removed in followup refactor + // PR that updates the flyout classes to use inflaters. + return this.svgGroup; } /** Correctly position the flyout button and make it visible. */ @@ -235,6 +242,17 @@ export class FlyoutButton implements IASTNodeLocationSvg { this.updateTransform(); } + /** + * Move the element by a relative offset. + * + * @param dx Horizontal offset in workspace units. + * @param dy Vertical offset in workspace units. + * @param _reason Why is this move happening? 'user', 'bump', 'snap'... + */ + moveBy(dx: number, dy: number, _reason?: string[]) { + this.moveTo(this.position.x + dx, this.position.y + dy); + } + /** @returns Whether or not the button is a label. */ isLabel(): boolean { return this.isFlyoutLabel; @@ -250,6 +268,21 @@ export class FlyoutButton implements IASTNodeLocationSvg { return this.position; } + /** + * Returns the coordinates of a bounded element describing the dimensions of + * the element. Coordinate system: workspace coordinates. + * + * @returns Object with coordinates of the bounded element. + */ + getBoundingRectangle() { + return new Rect( + this.position.y, + this.position.y + this.height, + this.position.x, + this.position.x + this.width, + ); + } + /** @returns Text of the button. */ getButtonText(): string { return this.text; @@ -275,9 +308,8 @@ export class FlyoutButton implements IASTNodeLocationSvg { /** Dispose of this button. */ dispose() { - if (this.onMouseUpWrapper) { - browserEvents.unbind(this.onMouseUpWrapper); - } + browserEvents.unbind(this.onMouseDownWrapper); + browserEvents.unbind(this.onMouseUpWrapper); if (this.svgGroup) { dom.removeNode(this.svgGroup); } @@ -342,6 +374,21 @@ export class FlyoutButton implements IASTNodeLocationSvg { } } } + + private onMouseDown(e: PointerEvent) { + const gesture = this.targetWorkspace.getGesture(e); + const flyout = this.targetWorkspace.getFlyout(); + if (gesture && flyout) { + gesture.handleFlyoutStart(e, flyout); + } + } + + /** + * @returns The root SVG element of this rendered element. + */ + getSvgRoot() { + return this.svgGroup; + } } /** CSS for buttons and labels. See css.js for use. */ diff --git a/core/label_flyout_inflater.ts b/core/label_flyout_inflater.ts new file mode 100644 index 00000000000..67b02857a48 --- /dev/null +++ b/core/label_flyout_inflater.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; +import {FlyoutButton} from './flyout_button.js'; +import {ButtonOrLabelInfo} from './utils/toolbox.js'; +import * as registry from './registry.js'; + +/** + * Class responsible for creating labels for flyouts. + */ +export class LabelFlyoutInflater implements IFlyoutInflater { + /** + * Inflates a flyout label from the given state and adds it to the flyout. + * + * @param state A JSON representation of a flyout label. + * @param flyoutWorkspace The workspace to create the label on. + * @returns A FlyoutButton configured as a label. + */ + load(state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { + const label = new FlyoutButton( + flyoutWorkspace, + flyoutWorkspace.targetWorkspace!, + state as ButtonOrLabelInfo, + true, + ); + label.show(); + return label; + } + + /** + * Returns the amount of space that should follow this label. + * + * @param state A JSON representation of a flyout label. + * @param defaultGap The default spacing for flyout items. + * @returns The amount of space that should follow this label. + */ + gapForElement(state: Object, defaultGap: number): number { + return defaultGap; + } + + /** + * Disposes of the given label. + * + * @param element The flyout label to dispose of. + */ + disposeElement(element: IBoundedElement): void { + if (element instanceof FlyoutButton) { + element.dispose(); + } + } +} + +registry.register(registry.Type.FLYOUT_INFLATER, 'label', LabelFlyoutInflater); From bdc43bd0f74d42cc653d9566cd359af613bb6a63 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 27 Sep 2024 13:23:56 -0700 Subject: [PATCH 061/222] feat: Add support for inflating flyout separators. (#8592) * feat: Add support for inflating flyout separators. * chore: Add license. * chore: Add TSDoc. * refactor: Allow specifying an axis for flyout separators. --- core/flyout_separator.ts | 61 +++++++++++++++++++++++++++ core/separator_flyout_inflater.ts | 69 +++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 core/flyout_separator.ts create mode 100644 core/separator_flyout_inflater.ts diff --git a/core/flyout_separator.ts b/core/flyout_separator.ts new file mode 100644 index 00000000000..733371007a1 --- /dev/null +++ b/core/flyout_separator.ts @@ -0,0 +1,61 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import {Rect} from './utils/rect.js'; + +/** + * Representation of a gap between elements in a flyout. + */ +export class FlyoutSeparator implements IBoundedElement { + private x = 0; + private y = 0; + + /** + * Creates a new separator. + * + * @param gap The amount of space this separator should occupy. + * @param axis The axis along which this separator occupies space. + */ + constructor( + private gap: number, + private axis: SeparatorAxis, + ) {} + + /** + * Returns the bounding box of this separator. + * + * @returns The bounding box of this separator. + */ + getBoundingRectangle(): Rect { + switch (this.axis) { + case SeparatorAxis.X: + return new Rect(this.y, this.y, this.x, this.x + this.gap); + case SeparatorAxis.Y: + return new Rect(this.y, this.y + this.gap, this.x, this.x); + } + } + + /** + * Repositions this separator. + * + * @param dx The distance to move this separator on the X axis. + * @param dy The distance to move this separator on the Y axis. + * @param _reason The reason this move was initiated. + */ + moveBy(dx: number, dy: number, _reason?: string[]) { + this.x += dx; + this.y += dy; + } +} + +/** + * Representation of an axis along which a separator occupies space. + */ +export const enum SeparatorAxis { + X = 'x', + Y = 'y', +} diff --git a/core/separator_flyout_inflater.ts b/core/separator_flyout_inflater.ts new file mode 100644 index 00000000000..5ed02aeb978 --- /dev/null +++ b/core/separator_flyout_inflater.ts @@ -0,0 +1,69 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; +import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; +import type {SeparatorInfo} from './utils/toolbox.js'; +import * as registry from './registry.js'; + +/** + * Class responsible for creating separators for flyouts. + */ +export class SeparatorFlyoutInflater implements IFlyoutInflater { + /** + * Inflates a dummy flyout separator. + * + * The flyout automatically creates separators between every element with a + * size determined by calling gapForElement on the relevant inflater. + * Additionally, users can explicitly add separators in the flyout definition. + * When separators (implicitly or explicitly created) follow one another, the + * gap of the last one propagates backwards and flattens to one separator. + * This flattening is not additive; if there are initially separators of 2, 3, + * and 4 pixels, after normalization there will be one separator of 4 pixels. + * Therefore, this method returns a zero-width separator, which will be + * replaced by the one implicitly created by the flyout based on the value + * returned by gapForElement, which knows the default gap, unlike this method. + * + * @param _state A JSON representation of a flyout separator. + * @param flyoutWorkspace The workspace the separator belongs to. + * @returns A newly created FlyoutSeparator. + */ + load(_state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { + const flyoutAxis = flyoutWorkspace.targetWorkspace?.getFlyout() + ?.horizontalLayout + ? SeparatorAxis.X + : SeparatorAxis.Y; + return new FlyoutSeparator(0, flyoutAxis); + } + + /** + * Returns the size of the separator. See `load` for more details. + * + * @param state A JSON representation of a flyout separator. + * @param defaultGap The default spacing for flyout items. + * @returns The desired size of the separator. + */ + gapForElement(state: Object, defaultGap: number): number { + const separatorState = state as SeparatorInfo; + const newGap = parseInt(String(separatorState['gap'])); + return newGap ?? defaultGap; + } + + /** + * Disposes of the given separator. Intentional no-op. + * + * @param _element The flyout separator to dispose of. + */ + disposeElement(_element: IBoundedElement): void {} +} + +registry.register( + registry.Type.FLYOUT_INFLATER, + 'sep', + SeparatorFlyoutInflater, +); From ec5b6e7f714e4a9fa67ecb57056079bba29e04d0 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 27 Sep 2024 14:12:59 -0700 Subject: [PATCH 062/222] feat: Add a BlockFlyoutInflater class. (#8591) * feat: Add a BlockFlyoutInflater class. * fix: Fix the capacity filter callback argument name. * fix: Fix addBlockListeners comment. * chore: Add license. * chore: Add TSDoc. * refactor: Make capacity filtering a normal method. * fix: Bind flyout filter to `this`. --- core/block_flyout_inflater.ts | 262 ++++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 core/block_flyout_inflater.ts diff --git a/core/block_flyout_inflater.ts b/core/block_flyout_inflater.ts new file mode 100644 index 00000000000..b22d2a82171 --- /dev/null +++ b/core/block_flyout_inflater.ts @@ -0,0 +1,262 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFlyout} from './interfaces/i_flyout.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import {BlockSvg} from './block_svg.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; +import * as utilsXml from './utils/xml.js'; +import * as eventUtils from './events/utils.js'; +import * as Xml from './xml.js'; +import * as blocks from './serialization/blocks.js'; +import * as common from './common.js'; +import * as registry from './registry.js'; +import {MANUALLY_DISABLED} from './constants.js'; +import type {Abstract as AbstractEvent} from './events/events_abstract.js'; +import type {BlockInfo} from './utils/toolbox.js'; +import * as browserEvents from './browser_events.js'; + +/** + * The language-neutral ID for when the reason why a block is disabled is + * because the workspace is at block capacity. + */ +const WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON = + 'WORKSPACE_AT_BLOCK_CAPACITY'; + +/** + * Class responsible for creating blocks for flyouts. + */ +export class BlockFlyoutInflater implements IFlyoutInflater { + protected permanentlyDisabledBlocks = new Set(); + protected listeners = new Map(); + protected flyoutWorkspace?: WorkspaceSvg; + protected flyout?: IFlyout; + private capacityWrapper: (event: AbstractEvent) => void; + + /** + * Creates a new BlockFlyoutInflater instance. + */ + constructor() { + this.capacityWrapper = this.filterFlyoutBasedOnCapacity.bind(this); + } + + /** + * Inflates a flyout block from the given state and adds it to the flyout. + * + * @param state A JSON representation of a flyout block. + * @param flyoutWorkspace The workspace to create the block on. + * @returns A newly created block. + */ + load(state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { + this.setFlyoutWorkspace(flyoutWorkspace); + this.flyout = flyoutWorkspace.targetWorkspace?.getFlyout() ?? undefined; + const block = this.createBlock(state as BlockInfo, flyoutWorkspace); + + if (!block.isEnabled()) { + // Record blocks that were initially disabled. + // Do not enable these blocks as a result of capacity filtering. + this.permanentlyDisabledBlocks.add(block); + } else { + this.updateStateBasedOnCapacity(block); + } + + // Mark blocks as being inside a flyout. This is used to detect and + // prevent the closure of the flyout if the user right-clicks on such + // a block. + block.getDescendants(false).forEach((b) => (b.isInFlyout = true)); + this.addBlockListeners(block); + + return block; + } + + /** + * Creates a block on the given workspace. + * + * @param blockDefinition A JSON representation of the block to create. + * @param workspace The workspace to create the block on. + * @returns The newly created block. + */ + createBlock(blockDefinition: BlockInfo, workspace: WorkspaceSvg): BlockSvg { + let block; + if (blockDefinition['blockxml']) { + const xml = ( + typeof blockDefinition['blockxml'] === 'string' + ? utilsXml.textToDom(blockDefinition['blockxml']) + : blockDefinition['blockxml'] + ) as Element; + block = Xml.domToBlockInternal(xml, workspace); + } else { + if (blockDefinition['enabled'] === undefined) { + blockDefinition['enabled'] = + blockDefinition['disabled'] !== 'true' && + blockDefinition['disabled'] !== true; + } + if ( + blockDefinition['disabledReasons'] === undefined && + blockDefinition['enabled'] === false + ) { + blockDefinition['disabledReasons'] = [MANUALLY_DISABLED]; + } + block = blocks.appendInternal(blockDefinition as blocks.State, workspace); + } + + return block as BlockSvg; + } + + /** + * Returns the amount of space that should follow this block. + * + * @param state A JSON representation of a flyout block. + * @param defaultGap The default spacing for flyout items. + * @returns The amount of space that should follow this block. + */ + gapForElement(state: Object, defaultGap: number): number { + const blockState = state as BlockInfo; + let gap; + if (blockState['gap']) { + gap = parseInt(String(blockState['gap'])); + } else if (blockState['blockxml']) { + const xml = ( + typeof blockState['blockxml'] === 'string' + ? utilsXml.textToDom(blockState['blockxml']) + : blockState['blockxml'] + ) as Element; + gap = parseInt(xml.getAttribute('gap')!); + } + + return !gap || isNaN(gap) ? defaultGap : gap; + } + + /** + * Disposes of the given block. + * + * @param element The flyout block to dispose of. + */ + disposeElement(element: IBoundedElement): void { + if (!(element instanceof BlockSvg)) return; + this.removeListeners(element.id); + element.dispose(false, false); + } + + /** + * Removes event listeners for the block with the given ID. + * + * @param blockId The ID of the block to remove event listeners from. + */ + protected removeListeners(blockId: string) { + const blockListeners = this.listeners.get(blockId) ?? []; + blockListeners.forEach((l) => browserEvents.unbind(l)); + this.listeners.delete(blockId); + } + + /** + * Updates this inflater's flyout workspace. + * + * @param workspace The workspace of the flyout that owns this inflater. + */ + protected setFlyoutWorkspace(workspace: WorkspaceSvg) { + if (this.flyoutWorkspace === workspace) return; + + if (this.flyoutWorkspace) { + this.flyoutWorkspace.targetWorkspace?.removeChangeListener( + this.capacityWrapper, + ); + } + this.flyoutWorkspace = workspace; + this.flyoutWorkspace.targetWorkspace?.addChangeListener( + this.capacityWrapper, + ); + } + + /** + * Updates the enabled state of the given block based on the capacity of the + * workspace. + * + * @param block The block to update the enabled/disabled state of. + */ + private updateStateBasedOnCapacity(block: BlockSvg) { + const enable = this.flyoutWorkspace?.targetWorkspace?.isCapacityAvailable( + common.getBlockTypeCounts(block), + ); + let currentBlock: BlockSvg | null = block; + while (currentBlock) { + currentBlock.setDisabledReason( + !enable, + WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON, + ); + currentBlock = currentBlock.getNextBlock(); + } + } + + /** + * Add listeners to a block that has been added to the flyout. + * + * @param block The block to add listeners for. + */ + protected addBlockListeners(block: BlockSvg) { + const blockListeners = []; + + blockListeners.push( + browserEvents.conditionalBind( + block.getSvgRoot(), + 'pointerdown', + block, + (e: PointerEvent) => { + const gesture = this.flyoutWorkspace?.targetWorkspace?.getGesture(e); + const flyout = this.flyoutWorkspace?.targetWorkspace?.getFlyout(); + if (gesture && flyout) { + gesture.setStartBlock(block); + gesture.handleFlyoutStart(e, flyout); + } + }, + ), + ); + + blockListeners.push( + browserEvents.bind(block.getSvgRoot(), 'pointerenter', null, () => { + if (!this.flyoutWorkspace?.targetWorkspace?.isDragging()) { + block.addSelect(); + } + }), + ); + blockListeners.push( + browserEvents.bind(block.getSvgRoot(), 'pointerleave', null, () => { + if (!this.flyoutWorkspace?.targetWorkspace?.isDragging()) { + block.removeSelect(); + } + }), + ); + + this.listeners.set(block.id, blockListeners); + } + + /** + * Updates the state of blocks in our owning flyout to be disabled/enabled + * based on the capacity of the workspace for more blocks of that type. + * + * @param event The event that triggered this update. + */ + private filterFlyoutBasedOnCapacity(event: AbstractEvent) { + if ( + !this.flyoutWorkspace || + (event && + !( + event.type === eventUtils.BLOCK_CREATE || + event.type === eventUtils.BLOCK_DELETE + )) + ) + return; + + this.flyoutWorkspace.getTopBlocks(false).forEach((block) => { + if (!this.permanentlyDisabledBlocks.has(block)) { + this.updateStateBasedOnCapacity(block); + } + }); + } +} + +registry.register(registry.Type.FLYOUT_INFLATER, 'block', BlockFlyoutInflater); From a4b522781cda9e6745c193da17da59f7732b4c73 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 2 Oct 2024 08:18:21 -0700 Subject: [PATCH 063/222] fix: Fix bug that prevented dismissing the widgetdiv in a mutator workspace. (#8600) * fix: Fix bug that prevented dismissing the widgetdiv in a mutator workspace. * fix: Check if the correct workspace is null. * fix: Remove errant this. --- core/widgetdiv.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/core/widgetdiv.ts b/core/widgetdiv.ts index 9f58bb1c544..b5edc977448 100644 --- a/core/widgetdiv.ts +++ b/core/widgetdiv.ts @@ -166,10 +166,22 @@ export function hideIfOwner(oldOwner: unknown) { * Destroy the widget and hide the div if it is being used by an object in the * specified workspace, or if it is used by an unknown workspace. * - * @param oldOwnerWorkspace The workspace that was using this container. + * @param workspace The workspace that was using this container. */ -export function hideIfOwnerIsInWorkspace(oldOwnerWorkspace: WorkspaceSvg) { - if (ownerWorkspace === null || ownerWorkspace === oldOwnerWorkspace) { +export function hideIfOwnerIsInWorkspace(workspace: WorkspaceSvg) { + let ownerIsInWorkspace = ownerWorkspace === null; + // Check if the given workspace is a parent workspace of the one containing + // our owner. + let currentWorkspace: WorkspaceSvg | null = workspace; + while (!ownerIsInWorkspace && currentWorkspace) { + if (currentWorkspace === workspace) { + ownerIsInWorkspace = true; + break; + } + currentWorkspace = workspace.options.parentWorkspace; + } + + if (ownerIsInWorkspace) { hide(); } } From e5c1a89cdfe0f5d76bc38c79fc7e1a202e9de3c8 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 2 Oct 2024 08:18:47 -0700 Subject: [PATCH 064/222] fix: Fix bug that caused fields in the flyout to use the main workspace's scale. (#8607) * fix: Fix bug that caused fields in the flyout to use the main workspace's scale. * chore: remove errant param in docs. --- core/field_input.ts | 2 +- core/workspace_svg.ts | 62 ++++++++++++++++++++++++++--- tests/mocha/field_textinput_test.js | 2 +- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/core/field_input.ts b/core/field_input.ts index 3326cd35f48..dcc2ac29ec4 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -415,7 +415,7 @@ export abstract class FieldInput extends Field< 'spellcheck', this.spellcheck_ as AnyDuringMigration, ); - const scale = this.workspace_!.getScale(); + const scale = this.workspace_!.getAbsoluteScale(); const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt'; div!.style.fontSize = fontSize; htmlInput.style.fontSize = fontSize; diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 7c57f47cdbf..5447bdf51b8 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -29,6 +29,7 @@ import * as ContextMenu from './contextmenu.js'; import {ContextMenuRegistry} from './contextmenu_registry.js'; import * as dropDownDiv from './dropdowndiv.js'; import * as eventUtils from './events/utils.js'; +import {Flyout} from './flyout_base.js'; import type {FlyoutButton} from './flyout_button.js'; import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; @@ -2022,18 +2023,69 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { } /** - * Get the workspace's zoom factor. If the workspace has a parent, we call - * into the parent to get the workspace scale. + * Get the workspace's zoom factor. * * @returns The workspace zoom factor. Units: (pixels / workspaceUnit). */ getScale(): number { - if (this.options.parentWorkspace) { - return this.options.parentWorkspace.getScale(); - } return this.scale; } + /** + * Returns the absolute scale of the workspace. + * + * Workspace scaling is multiplicative; if a workspace B (e.g. a mutator editor) + * with scale Y is nested within a root workspace A with scale X, workspace B's + * effective scale is X * Y, because, as a child of A, it is already transformed + * by A's scaling factor, and then further transforms itself by its own scaling + * factor. Normally this Just Works, but for global elements (e.g. field + * editors) that are visually associated with a particular workspace but live at + * the top level of the DOM rather than being a child of their associated + * workspace, the absolute/effective scale may be needed to render + * appropriately. + * + * @returns The absolute/effective scale of the given workspace. + */ + getAbsoluteScale() { + // Returns a workspace's own scale, without regard to multiplicative scaling. + const getLocalScale = (workspace: WorkspaceSvg): number => { + // Workspaces in flyouts may have a distinct scale; use this if relevant. + if (workspace.isFlyout) { + const flyout = workspace.targetWorkspace?.getFlyout(); + if (flyout instanceof Flyout) { + return flyout.getFlyoutScale(); + } + } + + return workspace.getScale(); + }; + + const computeScale = (workspace: WorkspaceSvg, scale: number): number => { + // If the workspace has no parent, or it does have a parent but is not + // actually a child of its parent workspace in the DOM (this is the case for + // flyouts in the main workspace), we're done; just return the scale so far + // multiplied by the workspace's own scale. + if ( + !workspace.options.parentWorkspace || + !workspace.options.parentWorkspace + .getSvgGroup() + .contains(workspace.getSvgGroup()) + ) { + return scale * getLocalScale(workspace); + } + + // If there is a parent workspace, and this workspace is a child of it in + // the DOM, scales are multiplicative, so recurse up the workspace + // hierarchy. + return computeScale( + workspace.options.parentWorkspace, + scale * getLocalScale(workspace), + ); + }; + + return computeScale(this, 1); + } + /** * Scroll the workspace to a specified offset (in pixels), keeping in the * workspace bounds. See comment on workspaceSvg.scrollX for more detail on diff --git a/tests/mocha/field_textinput_test.js b/tests/mocha/field_textinput_test.js index 7b0da1b4cca..3561d360a3f 100644 --- a/tests/mocha/field_textinput_test.js +++ b/tests/mocha/field_textinput_test.js @@ -192,7 +192,7 @@ suite('Text Input Fields', function () { setup(function () { this.prepField = function (field) { const workspace = { - getScale: function () { + getAbsoluteScale: function () { return 1; }, getRenderer: function () { From e777086f1693e38fcb3eb3c2ee1b7e38d4c4917f Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 2 Oct 2024 09:20:45 -0700 Subject: [PATCH 065/222] refactor!: Update flyouts to use inflaters. (#8601) * refactor: Update flyouts to use inflaters. * fix: Specify an axis when creating flyout separators. * chore: Remove unused import. * chore: Fix tests. * chore: Update documentation. * chore: Improve code readability. * refactor: Use null instead of undefined. --- core/blockly.ts | 12 + core/flyout_base.ts | 637 ++++++---------------------------- core/flyout_horizontal.ts | 69 +--- core/flyout_vertical.ts | 91 +---- core/keyboard_nav/ast_node.ts | 16 +- tests/mocha/flyout_test.js | 55 +-- 6 files changed, 176 insertions(+), 704 deletions(-) diff --git a/core/blockly.ts b/core/blockly.ts index 28eb0010a6b..fda49830f5f 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -96,6 +96,12 @@ import { } from './field_variable.js'; import {Flyout} from './flyout_base.js'; import {FlyoutButton} from './flyout_button.js'; +import {FlyoutSeparator} from './flyout_separator.js'; +import {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import {BlockFlyoutInflater} from './block_flyout_inflater.js'; +import {ButtonFlyoutInflater} from './button_flyout_inflater.js'; +import {LabelFlyoutInflater} from './label_flyout_inflater.js'; +import {SeparatorFlyoutInflater} from './separator_flyout_inflater.js'; import {HorizontalFlyout} from './flyout_horizontal.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import {VerticalFlyout} from './flyout_vertical.js'; @@ -510,6 +516,12 @@ export { export {Flyout}; export {FlyoutButton}; export {FlyoutMetricsManager}; +export {FlyoutSeparator}; +export {IFlyoutInflater}; +export {BlockFlyoutInflater}; +export {ButtonFlyoutInflater}; +export {LabelFlyoutInflater}; +export {SeparatorFlyoutInflater}; export {CodeGenerator}; export {CodeGenerator as Generator}; // Deprecated name, October 2022. export {Gesture}; diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 2ea85a0dfdd..0c62f3b4c2b 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -12,21 +12,18 @@ // Former goog.module ID: Blockly.Flyout import type {Abstract as AbstractEvent} from './events/events_abstract.js'; -import type {Block} from './block.js'; import {BlockSvg} from './block_svg.js'; import * as browserEvents from './browser_events.js'; -import * as common from './common.js'; import {ComponentManager} from './component_manager.js'; import {DeleteArea} from './delete_area.js'; import * as eventUtils from './events/utils.js'; -import {FlyoutButton} from './flyout_button.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import type {IFlyout} from './interfaces/i_flyout.js'; -import {MANUALLY_DISABLED} from './constants.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {Options} from './options.js'; import {ScrollbarPair} from './scrollbar_pair.js'; import * as blocks from './serialization/blocks.js'; -import * as Tooltip from './tooltip.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import * as idGenerator from './utils/idgenerator.js'; @@ -34,22 +31,10 @@ import {Svg} from './utils/svg.js'; import * as toolbox from './utils/toolbox.js'; import * as Variables from './variables.js'; import {WorkspaceSvg} from './workspace_svg.js'; -import * as utilsXml from './utils/xml.js'; -import * as Xml from './xml.js'; +import * as registry from './registry.js'; import * as renderManagement from './render_management.js'; import {IAutoHideable} from './interfaces/i_autohideable.js'; - -enum FlyoutItemType { - BLOCK = 'block', - BUTTON = 'button', -} - -/** - * The language-neutral ID for when the reason why a block is disabled is - * because the workspace is at block capacity. - */ -const WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON = - 'WORKSPACE_AT_BLOCK_CAPACITY'; +import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; /** * Class for a flyout. @@ -84,12 +69,11 @@ export abstract class Flyout protected abstract setMetrics_(xyRatio: {x?: number; y?: number}): void; /** - * Lay out the blocks in the flyout. + * Lay out the elements in the flyout. * - * @param contents The blocks and buttons to lay out. - * @param gaps The visible gaps between blocks. + * @param contents The flyout elements to lay out. */ - protected abstract layout_(contents: FlyoutItem[], gaps: number[]): void; + protected abstract layout_(contents: FlyoutItem[]): void; /** * Scroll the flyout. @@ -99,8 +83,8 @@ export abstract class Flyout protected abstract wheel_(e: WheelEvent): void; /** - * Compute height of flyout. Position mat under each block. - * For RTL: Lay out the blocks right-aligned. + * Compute bounds of flyout. + * For RTL: Lay out the elements right-aligned. */ protected abstract reflowInternal_(): void; @@ -123,11 +107,6 @@ export abstract class Flyout */ abstract scrollToStart(): void; - /** - * The type of a flyout content item. - */ - static FlyoutItemType = FlyoutItemType; - protected workspace_: WorkspaceSvg; RTL: boolean; /** @@ -147,43 +126,15 @@ export abstract class Flyout /** * Function that will be registered as a change listener on the workspace - * to reflow when blocks in the flyout workspace change. + * to reflow when elements in the flyout workspace change. */ private reflowWrapper: ((e: AbstractEvent) => void) | null = null; /** - * Function that disables blocks in the flyout based on max block counts - * allowed in the target workspace. Registered as a change listener on the - * target workspace. - */ - private filterWrapper: ((e: AbstractEvent) => void) | null = null; - - /** - * List of background mats that lurk behind each block to catch clicks - * landing in the blocks' lakes and bays. - */ - private mats: SVGElement[] = []; - - /** - * List of visible buttons. - */ - protected buttons_: FlyoutButton[] = []; - - /** - * List of visible buttons and blocks. + * List of flyout elements. */ protected contents: FlyoutItem[] = []; - /** - * List of event listeners. - */ - private listeners: browserEvents.Data[] = []; - - /** - * List of blocks that should always be disabled. - */ - private permanentlyDisabled: Block[] = []; - protected readonly tabWidth_: number; /** @@ -193,11 +144,6 @@ export abstract class Flyout */ targetWorkspace!: WorkspaceSvg; - /** - * A list of blocks that can be reused. - */ - private recycledBlocks: BlockSvg[] = []; - /** * Does the flyout automatically close when a block is created? */ @@ -212,7 +158,6 @@ export abstract class Flyout * Whether the workspace containing this flyout is visible. */ private containerVisible = true; - protected rectMap_: WeakMap; /** * Corner radius of the flyout background. @@ -270,6 +215,13 @@ export abstract class Flyout * The root SVG group for the button or label. */ protected svgGroup_: SVGGElement | null = null; + + /** + * Map from flyout content type to the corresponding inflater class + * responsible for creating concrete instances of the content type. + */ + protected inflaters = new Map(); + /** * @param workspaceOptions Dictionary of options for the * workspace. @@ -309,15 +261,7 @@ export abstract class Flyout this.tabWidth_ = this.workspace_.getRenderer().getConstants().TAB_WIDTH; /** - * A map from blocks to the rects which are beneath them to act as input - * targets. - * - * @internal - */ - this.rectMap_ = new WeakMap(); - - /** - * Margin around the edges of the blocks in the flyout. + * Margin around the edges of the elements in the flyout. */ this.MARGIN = this.CORNER_RADIUS; @@ -402,15 +346,6 @@ export abstract class Flyout this.wheel_, ), ); - this.filterWrapper = (event) => { - if ( - event.type === eventUtils.BLOCK_CREATE || - event.type === eventUtils.BLOCK_DELETE - ) { - this.filterForCapacity(); - } - }; - this.targetWorkspace.addChangeListener(this.filterWrapper); // Dragging the flyout up and down. this.boundEvents.push( @@ -454,9 +389,6 @@ export abstract class Flyout browserEvents.unbind(event); } this.boundEvents.length = 0; - if (this.filterWrapper) { - this.targetWorkspace.removeChangeListener(this.filterWrapper); - } if (this.workspace_) { this.workspace_.getThemeManager().unsubscribe(this.svgBackground_!); this.workspace_.dispose(); @@ -576,16 +508,16 @@ export abstract class Flyout } /** - * Get the list of buttons and blocks of the current flyout. + * Get the list of elements of the current flyout. * - * @returns The array of flyout buttons and blocks. + * @returns The array of flyout elements. */ getContents(): FlyoutItem[] { return this.contents; } /** - * Store the list of buttons and blocks on the flyout. + * Store the list of elements on the flyout. * * @param contents - The array of items for the flyout. */ @@ -660,16 +592,11 @@ export abstract class Flyout return; } this.setVisible(false); - // Delete all the event listeners. - for (const listen of this.listeners) { - browserEvents.unbind(listen); - } - this.listeners.length = 0; if (this.reflowWrapper) { this.workspace_.removeChangeListener(this.reflowWrapper); this.reflowWrapper = null; } - // Do NOT delete the blocks here. Wait until Flyout.show. + // Do NOT delete the flyout contents here. Wait until Flyout.show. // https://neil.fraser.name/news/2014/08/09/ } @@ -697,9 +624,9 @@ export abstract class Flyout renderManagement.triggerQueuedRenders(this.workspace_); - this.setContents(flyoutInfo.contents); + this.setContents(flyoutInfo); - this.layout_(flyoutInfo.contents, flyoutInfo.gaps); + this.layout_(flyoutInfo); if (this.horizontalLayout) { this.height_ = 0; @@ -709,8 +636,6 @@ export abstract class Flyout this.workspace_.setResizesEnabled(true); this.reflow(); - this.filterForCapacity(); - // Listen for block change events, and reflow the flyout in response. This // accommodates e.g. resizing a non-autoclosing flyout in response to the // user typing long strings into fields on the blocks in the flyout. @@ -723,7 +648,6 @@ export abstract class Flyout } }; this.workspace_.addChangeListener(this.reflowWrapper); - this.emptyRecycledBlocks(); } /** @@ -732,15 +656,12 @@ export abstract class Flyout * * @param parsedContent The array * of objects to show in the flyout. - * @returns The list of contents and gaps needed to lay out the flyout. + * @returns The list of contents needed to lay out the flyout. */ - private createFlyoutInfo(parsedContent: toolbox.FlyoutItemInfoArray): { - contents: FlyoutItem[]; - gaps: number[]; - } { + private createFlyoutInfo( + parsedContent: toolbox.FlyoutItemInfoArray, + ): FlyoutItem[] { const contents: FlyoutItem[] = []; - const gaps: number[] = []; - this.permanentlyDisabled.length = 0; const defaultGap = this.horizontalLayout ? this.GAP_X : this.GAP_Y; for (const info of parsedContent) { if ('custom' in info) { @@ -749,44 +670,58 @@ export abstract class Flyout const flyoutDef = this.getDynamicCategoryContents(categoryName); const parsedDynamicContent = toolbox.convertFlyoutDefToJsonArray(flyoutDef); - const {contents: dynamicContents, gaps: dynamicGaps} = - this.createFlyoutInfo(parsedDynamicContent); - contents.push(...dynamicContents); - gaps.push(...dynamicGaps); + contents.push(...this.createFlyoutInfo(parsedDynamicContent)); } - switch (info['kind'].toUpperCase()) { - case 'BLOCK': { - const blockInfo = info as toolbox.BlockInfo; - const block = this.createFlyoutBlock(blockInfo); - contents.push({type: FlyoutItemType.BLOCK, block: block}); - this.addBlockGap(blockInfo, gaps, defaultGap); - break; - } - case 'SEP': { - const sepInfo = info as toolbox.SeparatorInfo; - this.addSeparatorGap(sepInfo, gaps, defaultGap); - break; - } - case 'LABEL': { - const labelInfo = info as toolbox.LabelInfo; - // A label is a button with different styling. - const label = this.createButton(labelInfo, /** isLabel */ true); - contents.push({type: FlyoutItemType.BUTTON, button: label}); - gaps.push(defaultGap); - break; - } - case 'BUTTON': { - const buttonInfo = info as toolbox.ButtonInfo; - const button = this.createButton(buttonInfo, /** isLabel */ false); - contents.push({type: FlyoutItemType.BUTTON, button: button}); - gaps.push(defaultGap); - break; + const type = info['kind'].toLowerCase(); + const inflater = this.getInflaterForType(type); + if (inflater) { + const element = inflater.load(info, this.getWorkspace()); + contents.push({ + type, + element, + }); + const gap = inflater.gapForElement(info, defaultGap); + if (gap) { + contents.push({ + type: 'sep', + element: new FlyoutSeparator( + gap, + this.horizontalLayout ? SeparatorAxis.X : SeparatorAxis.Y, + ), + }); } } } - return {contents: contents, gaps: gaps}; + return this.normalizeSeparators(contents); + } + + /** + * Updates and returns the provided list of flyout contents to flatten + * separators as needed. + * + * When multiple separators occur one after another, the value of the last one + * takes precedence and the earlier separators in the group are removed. + * + * @param contents The list of flyout contents to flatten separators in. + * @returns An updated list of flyout contents with only one separator between + * each non-separator item. + */ + protected normalizeSeparators(contents: FlyoutItem[]): FlyoutItem[] { + for (let i = contents.length - 1; i > 0; i--) { + const elementType = contents[i].type.toLowerCase(); + const previousElementType = contents[i - 1].type.toLowerCase(); + if (elementType === 'sep' && previousElementType === 'sep') { + // Remove previousElement from the array, shifting the current element + // forward as a result. This preserves the behavior where explicit + // separator elements override the value of prior implicit (or explicit) + // separator elements. + contents.splice(i - 1, 1); + } + } + + return contents; } /** @@ -813,287 +748,18 @@ export abstract class Flyout } /** - * Creates a flyout button or a flyout label. - * - * @param btnInfo The object holding information about a button or a label. - * @param isLabel True if the button is a label, false otherwise. - * @returns The object used to display the button in the - * flyout. - */ - private createButton( - btnInfo: toolbox.ButtonOrLabelInfo, - isLabel: boolean, - ): FlyoutButton { - const curButton = new FlyoutButton( - this.workspace_, - this.targetWorkspace as WorkspaceSvg, - btnInfo, - isLabel, - ); - return curButton; - } - - /** - * Create a block from the xml and permanently disable any blocks that were - * defined as disabled. - * - * @param blockInfo The info of the block. - * @returns The block created from the blockInfo. - */ - private createFlyoutBlock(blockInfo: toolbox.BlockInfo): BlockSvg { - let block; - if (blockInfo['blockxml']) { - const xml = ( - typeof blockInfo['blockxml'] === 'string' - ? utilsXml.textToDom(blockInfo['blockxml']) - : blockInfo['blockxml'] - ) as Element; - block = this.getRecycledBlock(xml.getAttribute('type')!); - if (!block) { - block = Xml.domToBlockInternal(xml, this.workspace_); - } - } else { - block = this.getRecycledBlock(blockInfo['type']!); - if (!block) { - if (blockInfo['enabled'] === undefined) { - blockInfo['enabled'] = - blockInfo['disabled'] !== 'true' && blockInfo['disabled'] !== true; - } - if ( - blockInfo['disabledReasons'] === undefined && - blockInfo['enabled'] === false - ) { - blockInfo['disabledReasons'] = [MANUALLY_DISABLED]; - } - block = blocks.appendInternal( - blockInfo as blocks.State, - this.workspace_, - ); - } - } - - if (!block.isEnabled()) { - // Record blocks that were initially disabled. - // Do not enable these blocks as a result of capacity filtering. - this.permanentlyDisabled.push(block); - } - return block as BlockSvg; - } - - /** - * Returns a block from the array of recycled blocks with the given type, or - * undefined if one cannot be found. - * - * @param blockType The type of the block to try to recycle. - * @returns The recycled block, or undefined if - * one could not be recycled. - */ - private getRecycledBlock(blockType: string): BlockSvg | undefined { - let index = -1; - for (let i = 0; i < this.recycledBlocks.length; i++) { - if (this.recycledBlocks[i].type === blockType) { - index = i; - break; - } - } - return index === -1 ? undefined : this.recycledBlocks.splice(index, 1)[0]; - } - - /** - * Adds a gap in the flyout based on block info. - * - * @param blockInfo Information about a block. - * @param gaps The list of gaps between items in the flyout. - * @param defaultGap The default gap between one element and the - * next. - */ - private addBlockGap( - blockInfo: toolbox.BlockInfo, - gaps: number[], - defaultGap: number, - ) { - let gap; - if (blockInfo['gap']) { - gap = parseInt(String(blockInfo['gap'])); - } else if (blockInfo['blockxml']) { - const xml = ( - typeof blockInfo['blockxml'] === 'string' - ? utilsXml.textToDom(blockInfo['blockxml']) - : blockInfo['blockxml'] - ) as Element; - gap = parseInt(xml.getAttribute('gap')!); - } - gaps.push(!gap || isNaN(gap) ? defaultGap : gap); - } - - /** - * Add the necessary gap in the flyout for a separator. - * - * @param sepInfo The object holding - * information about a separator. - * @param gaps The list gaps between items in the flyout. - * @param defaultGap The default gap between the button and next - * element. - */ - private addSeparatorGap( - sepInfo: toolbox.SeparatorInfo, - gaps: number[], - defaultGap: number, - ) { - // Change the gap between two toolbox elements. - // - // The default gap is 24, can be set larger or smaller. - // This overwrites the gap attribute on the previous element. - const newGap = parseInt(String(sepInfo['gap'])); - // Ignore gaps before the first block. - if (!isNaN(newGap) && gaps.length > 0) { - gaps[gaps.length - 1] = newGap; - } else { - gaps.push(defaultGap); - } - } - - /** - * Delete blocks, mats and buttons from a previous showing of the flyout. + * Delete elements from a previous showing of the flyout. */ private clearOldBlocks() { - // Delete any blocks from a previous showing. - const oldBlocks = this.workspace_.getTopBlocks(false); - for (let i = 0, block; (block = oldBlocks[i]); i++) { - if (this.blockIsRecyclable_(block)) { - this.recycleBlock(block); - } else { - block.dispose(false, false); - } - } - // Delete any mats from a previous showing. - for (let j = 0; j < this.mats.length; j++) { - const rect = this.mats[j]; - if (rect) { - Tooltip.unbindMouseEvents(rect); - dom.removeNode(rect); - } - } - this.mats.length = 0; - // Delete any buttons from a previous showing. - for (let i = 0, button; (button = this.buttons_[i]); i++) { - button.dispose(); - } - this.buttons_.length = 0; + this.getContents().forEach((element) => { + const inflater = this.getInflaterForType(element.type); + inflater?.disposeElement(element.element); + }); // Clear potential variables from the previous showing. this.workspace_.getPotentialVariableMap()?.clear(); } - /** - * Empties all of the recycled blocks, properly disposing of them. - */ - private emptyRecycledBlocks() { - for (let i = 0; i < this.recycledBlocks.length; i++) { - this.recycledBlocks[i].dispose(); - } - this.recycledBlocks = []; - } - - /** - * Returns whether the given block can be recycled or not. - * - * @param _block The block to check for recyclability. - * @returns True if the block can be recycled. False otherwise. - */ - protected blockIsRecyclable_(_block: BlockSvg): boolean { - // By default, recycling is disabled. - return false; - } - - /** - * Puts a previously created block into the recycle bin and moves it to the - * top of the workspace. Used during large workspace swaps to limit the number - * of new DOM elements we need to create. - * - * @param block The block to recycle. - */ - private recycleBlock(block: BlockSvg) { - const xy = block.getRelativeToSurfaceXY(); - block.moveBy(-xy.x, -xy.y); - this.recycledBlocks.push(block); - } - - /** - * Add listeners to a block that has been added to the flyout. - * - * @param root The root node of the SVG group the block is in. - * @param block The block to add listeners for. - * @param rect The invisible rectangle under the block that acts - * as a mat for that block. - */ - protected addBlockListeners_( - root: SVGElement, - block: BlockSvg, - rect: SVGElement, - ) { - this.listeners.push( - browserEvents.conditionalBind( - root, - 'pointerdown', - null, - this.blockMouseDown(block), - ), - ); - this.listeners.push( - browserEvents.conditionalBind( - rect, - 'pointerdown', - null, - this.blockMouseDown(block), - ), - ); - this.listeners.push( - browserEvents.bind(root, 'pointerenter', block, () => { - if (!this.targetWorkspace.isDragging()) { - block.addSelect(); - } - }), - ); - this.listeners.push( - browserEvents.bind(root, 'pointerleave', block, () => { - if (!this.targetWorkspace.isDragging()) { - block.removeSelect(); - } - }), - ); - this.listeners.push( - browserEvents.bind(rect, 'pointerenter', block, () => { - if (!this.targetWorkspace.isDragging()) { - block.addSelect(); - } - }), - ); - this.listeners.push( - browserEvents.bind(rect, 'pointerleave', block, () => { - if (!this.targetWorkspace.isDragging()) { - block.removeSelect(); - } - }), - ); - } - - /** - * Handle a pointerdown on an SVG block in a non-closing flyout. - * - * @param block The flyout block to copy. - * @returns Function to call when block is clicked. - */ - private blockMouseDown(block: BlockSvg): Function { - return (e: PointerEvent) => { - const gesture = this.targetWorkspace.getGesture(e); - if (gesture) { - gesture.setStartBlock(block); - gesture.handleFlyoutStart(e, this); - } - }; - } - /** * Pointer down on the flyout background. Start a vertical scroll drag. * @@ -1162,123 +828,12 @@ export abstract class Flyout } if (this.autoClose) { this.hide(); - } else { - this.filterForCapacity(); } return newBlock; } /** - * Initialize the given button: move it to the correct location, - * add listeners, etc. - * - * @param button The button to initialize and place. - * @param x The x position of the cursor during this layout pass. - * @param y The y position of the cursor during this layout pass. - */ - protected initFlyoutButton_(button: FlyoutButton, x: number, y: number) { - const buttonSvg = button.createDom(); - button.moveTo(x, y); - button.show(); - // Clicking on a flyout button or label is a lot like clicking on the - // flyout background. - this.listeners.push( - browserEvents.conditionalBind( - buttonSvg, - 'pointerdown', - this, - this.onMouseDown, - ), - ); - - this.buttons_.push(button); - } - - /** - * Create and place a rectangle corresponding to the given block. - * - * @param block The block to associate the rect to. - * @param x The x position of the cursor during this layout pass. - * @param y The y position of the cursor during this layout pass. - * @param blockHW The height and width of - * the block. - * @param index The index into the mats list where this rect should - * be placed. - * @returns Newly created SVG element for the rectangle behind - * the block. - */ - protected createRect_( - block: BlockSvg, - x: number, - y: number, - blockHW: {height: number; width: number}, - index: number, - ): SVGElement { - // Create an invisible rectangle under the block to act as a button. Just - // using the block as a button is poor, since blocks have holes in them. - const rect = dom.createSvgElement(Svg.RECT, { - 'fill-opacity': 0, - 'x': x, - 'y': y, - 'height': blockHW.height, - 'width': blockHW.width, - }); - (rect as AnyDuringMigration).tooltip = block; - Tooltip.bindMouseEvents(rect); - // Add the rectangles under the blocks, so that the blocks' tooltips work. - this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot()); - - this.rectMap_.set(block, rect); - this.mats[index] = rect; - return rect; - } - - /** - * Move a rectangle to sit exactly behind a block, taking into account tabs, - * hats, and any other protrusions we invent. - * - * @param rect The rectangle to move directly behind the block. - * @param block The block the rectangle should be behind. - */ - protected moveRectToBlock_(rect: SVGElement, block: BlockSvg) { - const blockHW = block.getHeightWidth(); - rect.setAttribute('width', String(blockHW.width)); - rect.setAttribute('height', String(blockHW.height)); - - const blockXY = block.getRelativeToSurfaceXY(); - rect.setAttribute('y', String(blockXY.y)); - rect.setAttribute( - 'x', - String(this.RTL ? blockXY.x - blockHW.width : blockXY.x), - ); - } - - /** - * Filter the blocks on the flyout to disable the ones that are above the - * capacity limit. For instance, if the user may only place two more blocks - * on the workspace, an "a + b" block that has two shadow blocks would be - * disabled. - */ - private filterForCapacity() { - const blocks = this.workspace_.getTopBlocks(false); - for (let i = 0, block; (block = blocks[i]); i++) { - if (!this.permanentlyDisabled.includes(block)) { - const enable = this.targetWorkspace.isCapacityAvailable( - common.getBlockTypeCounts(block), - ); - while (block) { - block.setDisabledReason( - !enable, - WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON, - ); - block = block.getNextBlock(); - } - } - } - } - - /** - * Reflow blocks and their mats. + * Reflow flyout contents. */ reflow() { if (this.reflowWrapper) { @@ -1377,13 +932,37 @@ export abstract class Flyout // No 'reason' provided since events are disabled. block.moveTo(new Coordinate(finalOffset.x, finalOffset.y)); } + + /** + * Returns the inflater responsible for constructing items of the given type. + * + * @param type The type of flyout content item to provide an inflater for. + * @returns An inflater object for the given type, or null if no inflater + * is registered for that type. + */ + protected getInflaterForType(type: string): IFlyoutInflater | null { + if (this.inflaters.has(type)) { + return this.inflaters.get(type) ?? null; + } + + const InflaterClass = registry.getClass( + registry.Type.FLYOUT_INFLATER, + type, + ); + if (InflaterClass) { + const inflater = new InflaterClass(); + this.inflaters.set(type, inflater); + return inflater; + } + + return null; + } } /** * A flyout content item. */ export interface FlyoutItem { - type: FlyoutItemType; - button?: FlyoutButton | undefined; - block?: BlockSvg | undefined; + type: string; + element: IBoundedElement; } diff --git a/core/flyout_horizontal.ts b/core/flyout_horizontal.ts index c23dede74f7..d19320c8297 100644 --- a/core/flyout_horizontal.ts +++ b/core/flyout_horizontal.ts @@ -14,7 +14,6 @@ import * as browserEvents from './browser_events.js'; import * as dropDownDiv from './dropdowndiv.js'; import {Flyout, FlyoutItem} from './flyout_base.js'; -import type {FlyoutButton} from './flyout_button.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import {Scrollbar} from './scrollbar.js'; @@ -252,10 +251,9 @@ export class HorizontalFlyout extends Flyout { /** * Lay out the blocks in the flyout. * - * @param contents The blocks and buttons to lay out. - * @param gaps The visible gaps between blocks. + * @param contents The flyout items to lay out. */ - protected override layout_(contents: FlyoutItem[], gaps: number[]) { + protected override layout_(contents: FlyoutItem[]) { this.workspace_.scale = this.targetWorkspace!.scale; const margin = this.MARGIN; let cursorX = margin + this.tabWidth_; @@ -264,43 +262,11 @@ export class HorizontalFlyout extends Flyout { contents = contents.reverse(); } - for (let i = 0, item; (item = contents[i]); i++) { - if (item.type === 'block') { - const block = item.block; - - if (block === undefined || block === null) { - continue; - } - - const allBlocks = block.getDescendants(false); - - for (let j = 0, child; (child = allBlocks[j]); j++) { - // Mark blocks as being inside a flyout. This is used to detect and - // prevent the closure of the flyout if the user right-clicks on such - // a block. - child.isInFlyout = true; - } - const root = block.getSvgRoot(); - const blockHW = block.getHeightWidth(); - // Figure out where to place the block. - const tab = block.outputConnection ? this.tabWidth_ : 0; - let moveX; - if (this.RTL) { - moveX = cursorX + blockHW.width; - } else { - moveX = cursorX - tab; - } - block.moveBy(moveX, cursorY); - - const rect = this.createRect_(block, moveX, cursorY, blockHW, i); - cursorX += blockHW.width + gaps[i]; - - this.addBlockListeners_(root, block, rect); - } else if (item.type === 'button') { - const button = item.button as FlyoutButton; - this.initFlyoutButton_(button, cursorX, cursorY); - cursorX += button.width + gaps[i]; - } + for (const item of contents) { + const rect = item.element.getBoundingRectangle(); + const moveX = this.RTL ? cursorX + rect.getWidth() : cursorX; + item.element.moveBy(moveX, cursorY); + cursorX += item.element.getBoundingRectangle().getWidth(); } } @@ -367,26 +333,17 @@ export class HorizontalFlyout extends Flyout { */ protected override reflowInternal_() { this.workspace_.scale = this.getFlyoutScale(); - let flyoutHeight = 0; - const blocks = this.workspace_.getTopBlocks(false); - for (let i = 0, block; (block = blocks[i]); i++) { - flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height); - } - const buttons = this.buttons_; - for (let i = 0, button; (button = buttons[i]); i++) { - flyoutHeight = Math.max(flyoutHeight, button.height); - } + let flyoutHeight = this.getContents().reduce((maxHeightSoFar, item) => { + return Math.max( + maxHeightSoFar, + item.element.getBoundingRectangle().getHeight(), + ); + }, 0); flyoutHeight += this.MARGIN * 1.5; flyoutHeight *= this.workspace_.scale; flyoutHeight += Scrollbar.scrollbarThickness; if (this.getHeight() !== flyoutHeight) { - for (let i = 0, block; (block = blocks[i]); i++) { - if (this.rectMap_.has(block)) { - this.moveRectToBlock_(this.rectMap_.get(block)!, block); - } - } - // TODO(#7689): Remove this. // Workspace with no scrollbars where this is permanently open on the top. // If scrollbars exist they properly update the metrics. diff --git a/core/flyout_vertical.ts b/core/flyout_vertical.ts index 374b0c33a54..8e7c1691c1d 100644 --- a/core/flyout_vertical.ts +++ b/core/flyout_vertical.ts @@ -14,7 +14,6 @@ import * as browserEvents from './browser_events.js'; import * as dropDownDiv from './dropdowndiv.js'; import {Flyout, FlyoutItem} from './flyout_base.js'; -import type {FlyoutButton} from './flyout_button.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import {Scrollbar} from './scrollbar.js'; @@ -221,51 +220,17 @@ export class VerticalFlyout extends Flyout { /** * Lay out the blocks in the flyout. * - * @param contents The blocks and buttons to lay out. - * @param gaps The visible gaps between blocks. + * @param contents The flyout items to lay out. */ - protected override layout_(contents: FlyoutItem[], gaps: number[]) { + protected override layout_(contents: FlyoutItem[]) { this.workspace_.scale = this.targetWorkspace!.scale; const margin = this.MARGIN; const cursorX = this.RTL ? margin : margin + this.tabWidth_; let cursorY = margin; - for (let i = 0, item; (item = contents[i]); i++) { - if (item.type === 'block') { - const block = item.block; - if (!block) { - continue; - } - const allBlocks = block.getDescendants(false); - for (let j = 0, child; (child = allBlocks[j]); j++) { - // Mark blocks as being inside a flyout. This is used to detect and - // prevent the closure of the flyout if the user right-clicks on such - // a block. - child.isInFlyout = true; - } - const root = block.getSvgRoot(); - const blockHW = block.getHeightWidth(); - const moveX = block.outputConnection - ? cursorX - this.tabWidth_ - : cursorX; - block.moveBy(moveX, cursorY); - - const rect = this.createRect_( - block, - this.RTL ? moveX - blockHW.width : moveX, - cursorY, - blockHW, - i, - ); - - this.addBlockListeners_(root, block, rect); - - cursorY += blockHW.height + gaps[i]; - } else if (item.type === 'button') { - const button = item.button as FlyoutButton; - this.initFlyoutButton_(button, cursorX, cursorY); - cursorY += button.height + gaps[i]; - } + for (const item of contents) { + item.element.moveBy(cursorX, cursorY); + cursorY += item.element.getBoundingRectangle().getHeight(); } } @@ -328,52 +293,32 @@ export class VerticalFlyout extends Flyout { } /** - * Compute width of flyout. toolbox.Position mat under each block. + * Compute width of flyout. * For RTL: Lay out the blocks and buttons to be right-aligned. */ protected override reflowInternal_() { this.workspace_.scale = this.getFlyoutScale(); - let flyoutWidth = 0; - const blocks = this.workspace_.getTopBlocks(false); - for (let i = 0, block; (block = blocks[i]); i++) { - let width = block.getHeightWidth().width; - if (block.outputConnection) { - width -= this.tabWidth_; - } - flyoutWidth = Math.max(flyoutWidth, width); - } - for (let i = 0, button; (button = this.buttons_[i]); i++) { - flyoutWidth = Math.max(flyoutWidth, button.width); - } + let flyoutWidth = this.getContents().reduce((maxWidthSoFar, item) => { + return Math.max( + maxWidthSoFar, + item.element.getBoundingRectangle().getWidth(), + ); + }, 0); flyoutWidth += this.MARGIN * 1.5 + this.tabWidth_; flyoutWidth *= this.workspace_.scale; flyoutWidth += Scrollbar.scrollbarThickness; if (this.getWidth() !== flyoutWidth) { - for (let i = 0, block; (block = blocks[i]); i++) { - if (this.RTL) { - // With the flyoutWidth known, right-align the blocks. - const oldX = block.getRelativeToSurfaceXY().x; - let newX = flyoutWidth / this.workspace_.scale - this.MARGIN; - if (!block.outputConnection) { - newX -= this.tabWidth_; - } - block.moveBy(newX - oldX, 0); - } - if (this.rectMap_.has(block)) { - this.moveRectToBlock_(this.rectMap_.get(block)!, block); - } - } if (this.RTL) { - // With the flyoutWidth known, right-align the buttons. - for (let i = 0, button; (button = this.buttons_[i]); i++) { - const y = button.getPosition().y; - const x = + // With the flyoutWidth known, right-align the flyout contents. + for (const item of this.getContents()) { + const oldX = item.element.getBoundingRectangle().left; + const newX = flyoutWidth / this.workspace_.scale - - button.width - + item.element.getBoundingRectangle().getWidth() - this.MARGIN - this.tabWidth_; - button.moveTo(x, y); + item.element.moveBy(newX - oldX, 0); } } diff --git a/core/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts index 7985ac6dc24..d71bef6a42d 100644 --- a/core/keyboard_nav/ast_node.ts +++ b/core/keyboard_nav/ast_node.ts @@ -13,6 +13,7 @@ // Former goog.module ID: Blockly.ASTNode import {Block} from '../block.js'; +import {BlockSvg} from '../block_svg.js'; import type {Connection} from '../connection.js'; import {ConnectionType} from '../connection_type.js'; import type {Field} from '../field.js'; @@ -347,10 +348,10 @@ export class ASTNode { ); if (!nextItem) return null; - if (nextItem.type === 'button' && nextItem.button) { - return ASTNode.createButtonNode(nextItem.button); - } else if (nextItem.type === 'block' && nextItem.block) { - return ASTNode.createStackNode(nextItem.block); + if (nextItem.element instanceof FlyoutButton) { + return ASTNode.createButtonNode(nextItem.element); + } else if (nextItem.element instanceof BlockSvg) { + return ASTNode.createStackNode(nextItem.element); } return null; @@ -370,12 +371,15 @@ export class ASTNode { forward: boolean, ): FlyoutItem | null { const currentIndex = flyoutContents.findIndex((item: FlyoutItem) => { - if (currentLocation instanceof Block && item.block === currentLocation) { + if ( + currentLocation instanceof BlockSvg && + item.element === currentLocation + ) { return true; } if ( currentLocation instanceof FlyoutButton && - item.button === currentLocation + item.element === currentLocation ) { return true; } diff --git a/tests/mocha/flyout_test.js b/tests/mocha/flyout_test.js index 522efbdc6e4..2240f264ea9 100644 --- a/tests/mocha/flyout_test.js +++ b/tests/mocha/flyout_test.js @@ -317,16 +317,12 @@ suite('Flyout', function () { function checkFlyoutInfo(flyoutSpy) { const flyoutInfo = flyoutSpy.returnValues[0]; - const contents = flyoutInfo.contents; - const gaps = flyoutInfo.gaps; + const contents = flyoutInfo; - const expectedGaps = [20, 24, 24]; - assert.deepEqual(gaps, expectedGaps); - - assert.equal(contents.length, 3, 'Contents'); + assert.equal(contents.length, 6, 'Contents'); assert.equal(contents[0].type, 'block', 'Contents'); - const block = contents[0]['block']; + const block = contents[0]['element']; assert.instanceOf(block, Blockly.BlockSvg); assert.equal(block.getFieldValue('OP'), 'NEQ'); const childA = block.getInputTargetBlock('A'); @@ -336,11 +332,20 @@ suite('Flyout', function () { assert.equal(childA.getFieldValue('NUM'), 1); assert.equal(childB.getFieldValue('NUM'), 2); - assert.equal(contents[1].type, 'button', 'Contents'); - assert.instanceOf(contents[1]['button'], Blockly.FlyoutButton); + assert.equal(contents[1].type, 'sep'); + assert.equal(contents[1].element.getBoundingRectangle().getHeight(), 20); assert.equal(contents[2].type, 'button', 'Contents'); - assert.instanceOf(contents[2]['button'], Blockly.FlyoutButton); + assert.instanceOf(contents[2]['element'], Blockly.FlyoutButton); + + assert.equal(contents[3].type, 'sep'); + assert.equal(contents[3].element.getBoundingRectangle().getHeight(), 24); + + assert.equal(contents[4].type, 'label', 'Contents'); + assert.instanceOf(contents[4]['element'], Blockly.FlyoutButton); + + assert.equal(contents[5].type, 'sep'); + assert.equal(contents[5].element.getBoundingRectangle().getHeight(), 24); } suite('Direct show', function () { @@ -629,35 +634,5 @@ suite('Flyout', function () { const block = this.flyout.workspace_.getAllBlocks()[0]; assert.equal(block.getFieldValue('NUM'), 321); }); - - test('Recycling enabled', function () { - this.flyout.blockIsRecyclable_ = function () { - return true; - }; - this.flyout.show({ - 'contents': [ - { - 'kind': 'BLOCK', - 'type': 'math_number', - 'fields': { - 'NUM': 123, - }, - }, - ], - }); - this.flyout.show({ - 'contents': [ - { - 'kind': 'BLOCK', - 'type': 'math_number', - 'fields': { - 'NUM': 321, - }, - }, - ], - }); - const block = this.flyout.workspace_.getAllBlocks()[0]; - assert.equal(block.getFieldValue('NUM'), 123); - }); }); }); From 14c9b1abcbf365467f435108da03825dc726afb2 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 2 Oct 2024 09:52:16 -0700 Subject: [PATCH 066/222] chore: remove obsolete comment. (#8606) --- core/generator.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/generator.ts b/core/generator.ts index 869e29a6a7c..85763b3cf76 100644 --- a/core/generator.ts +++ b/core/generator.ts @@ -252,8 +252,7 @@ export class CodeGenerator { return opt_thisOnly ? '' : this.blockToCode(block.getChildren(false)[0]); } - // Look up block generator function in dictionary - but fall back - // to looking up on this if not found, for backwards compatibility. + // Look up block generator function in dictionary. const func = this.forBlock[block.type]; if (typeof func !== 'function') { throw Error( From 2dfd8c30adcc6fd922d8084266be09c381bf0dc6 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 2 Oct 2024 10:32:44 -0700 Subject: [PATCH 067/222] feat: Allow specifying the placeholder text of workspace comments. (#8608) --- core/comments/comment_view.ts | 5 +++++ core/comments/rendered_workspace_comment.ts | 5 +++++ core/contextmenu_items.ts | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/comments/comment_view.ts b/core/comments/comment_view.ts index bda2b9762a8..798f51f961a 100644 --- a/core/comments/comment_view.ts +++ b/core/comments/comment_view.ts @@ -684,6 +684,11 @@ export class CommentView implements IRenderedElement { this.onTextChange(); } + /** Sets the placeholder text displayed for an empty comment. */ + setPlaceholderText(text: string) { + this.textArea.placeholder = text; + } + /** Registers a callback that listens for text changes. */ addTextChangeListener(listener: (oldText: string, newText: string) => void) { this.textChangeListeners.push(listener); diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index 79caf6a1d58..3144702ae09 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -104,6 +104,11 @@ export class RenderedWorkspaceComment this.view.setText(text); } + /** Sets the placeholder text displayed if the comment is empty. */ + setPlaceholderText(text: string): void { + this.view.setPlaceholderText(text); + } + /** Sets the size of the comment. */ override setSize(size: Size) { // setSize will trigger the change listener that updates diff --git a/core/contextmenu_items.ts b/core/contextmenu_items.ts index 254906ce7ff..f1d3293a4db 100644 --- a/core/contextmenu_items.ts +++ b/core/contextmenu_items.ts @@ -617,7 +617,7 @@ export function registerCommentCreate() { if (!workspace) return; eventUtils.setGroup(true); const comment = new RenderedWorkspaceComment(workspace); - comment.setText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']); + comment.setPlaceholderText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']); comment.moveTo( pixelsToWorkspaceCoords( new Coordinate(e.clientX, e.clientY), From 9fc693140a251feddd7905f81c5f3edbdf982b5c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Oct 2024 08:19:27 -0700 Subject: [PATCH 068/222] fix: Correctly calculate the bounds of hat blocks. (#8616) --- core/renderers/common/constants.ts | 5 ++++- core/renderers/common/info.ts | 1 - core/renderers/zelos/constants.ts | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/renderers/common/constants.ts b/core/renderers/common/constants.ts index c4ea9b24e5c..01217edb725 100644 --- a/core/renderers/common/constants.ts +++ b/core/renderers/common/constants.ts @@ -727,7 +727,10 @@ export class ConstantProvider { svgPaths.point(70, -height), svgPaths.point(width, 0), ]); - return {height, width, path: mainPath}; + // Height is actually the Y position of the control points defining the + // curve of the hat; the hat's actual rendered height is 3/4 of the control + // points' Y position, per https://stackoverflow.com/a/5327329 + return {height: height * 0.75, width, path: mainPath}; } /** diff --git a/core/renderers/common/info.ts b/core/renderers/common/info.ts index 995124c1b21..329c47442ea 100644 --- a/core/renderers/common/info.ts +++ b/core/renderers/common/info.ts @@ -232,7 +232,6 @@ export class RenderInfo { if (hasHat) { const hat = new Hat(this.constants_); this.topRow.elements.push(hat); - this.topRow.capline = hat.ascenderHeight; } else if (hasPrevious) { this.topRow.hasPreviousConnection = true; this.topRow.connection = new PreviousConnection( diff --git a/core/renderers/zelos/constants.ts b/core/renderers/zelos/constants.ts index 28c2cb4fc6c..ddb2bdeef37 100644 --- a/core/renderers/zelos/constants.ts +++ b/core/renderers/zelos/constants.ts @@ -290,7 +290,10 @@ export class ConstantProvider extends BaseConstantProvider { svgPaths.point(71, -height), svgPaths.point(width, 0), ]); - return {height, width, path: mainPath}; + // Height is actually the Y position of the control points defining the + // curve of the hat; the hat's actual rendered height is 3/4 of the control + // points' Y position, per https://stackoverflow.com/a/5327329 + return {height: height * 0.75, width, path: mainPath}; } /** From edd02f6955d9d391a04be3ac6acc93f0344078ad Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Oct 2024 08:19:49 -0700 Subject: [PATCH 069/222] fix: Take the flyout into account when positioning the workspace after a toolbox change. (#8617) * fix: Take the flyout into account when positioning the workspace after a toolbox change. * fix: Accomodate top-positioned toolboxes. --- core/toolbox/toolbox.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index cd91b2d8ae8..0c5a8e2a4f2 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -734,13 +734,18 @@ export class Toolbox // relative to the new absolute edge (ie toolbox edge). const workspace = this.workspace_; const rect = this.HtmlDiv!.getBoundingClientRect(); + const flyout = this.getFlyout(); const newX = this.toolboxPosition === toolbox.Position.LEFT - ? workspace.scrollX + rect.width + ? workspace.scrollX + + rect.width + + (flyout?.isVisible() ? flyout.getWidth() : 0) : workspace.scrollX; const newY = this.toolboxPosition === toolbox.Position.TOP - ? workspace.scrollY + rect.height + ? workspace.scrollY + + rect.height + + (flyout?.isVisible() ? flyout.getHeight() : 0) : workspace.scrollY; workspace.translate(newX, newY); From aeb1a806720c815c2496bb9a02b5e8228af68c61 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Oct 2024 08:20:34 -0700 Subject: [PATCH 070/222] feat: Allow specifying the default size of comments. (#8618) --- core/comments/comment_view.ts | 6 +++++- core/comments/workspace_comment.ts | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/comments/comment_view.ts b/core/comments/comment_view.ts index 798f51f961a..7146f21cae7 100644 --- a/core/comments/comment_view.ts +++ b/core/comments/comment_view.ts @@ -52,7 +52,7 @@ export class CommentView implements IRenderedElement { private textArea: HTMLTextAreaElement; /** The current size of the comment in workspace units. */ - private size: Size = new Size(120, 100); + private size: Size; /** Whether the comment is collapsed or not. */ private collapsed: boolean = false; @@ -102,6 +102,9 @@ export class CommentView implements IRenderedElement { /** Size of this comment when the resize drag was initiated. */ private preResizeSize?: Size; + /** The default size of newly created comments. */ + static defaultCommentSize = new Size(120, 100); + constructor(private readonly workspace: WorkspaceSvg) { this.svgRoot = dom.createSvgElement(Svg.G, { 'class': 'blocklyComment blocklyEditable blocklyDraggable', @@ -128,6 +131,7 @@ export class CommentView implements IRenderedElement { workspace.getLayerManager()?.append(this, layers.BLOCK); // Set size to the default size. + this.size = CommentView.defaultCommentSize; this.setSizeWithoutFiringEvents(this.size); // Set default transform (including inverted scale for RTL). diff --git a/core/comments/workspace_comment.ts b/core/comments/workspace_comment.ts index 0764b5168d2..9d50f74fc7d 100644 --- a/core/comments/workspace_comment.ts +++ b/core/comments/workspace_comment.ts @@ -11,6 +11,7 @@ import * as idGenerator from '../utils/idgenerator.js'; import * as eventUtils from '../events/utils.js'; import {CommentMove} from '../events/events_comment_move.js'; import {CommentResize} from '../events/events_comment_resize.js'; +import {CommentView} from './comment_view.js'; export class WorkspaceComment { /** The unique identifier for this comment. */ @@ -20,7 +21,7 @@ export class WorkspaceComment { private text = ''; /** The size of the comment in workspace units. */ - private size = new Size(120, 100); + private size: Size; /** Whether the comment is collapsed or not. */ private collapsed = false; @@ -55,6 +56,7 @@ export class WorkspaceComment { id?: string, ) { this.id = id && !workspace.getCommentById(id) ? id : idGenerator.genUid(); + this.size = CommentView.defaultCommentSize; workspace.addTopComment(this); From 089179bb016af6df063e25304bdb136242e43eeb Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 15 Oct 2024 13:45:39 -0700 Subject: [PATCH 071/222] fix: Fix exception when disposing of a workspace with a variable block obscuring a shadow block. (#8619) --- core/workspace.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/workspace.ts b/core/workspace.ts index 9e7d7c88432..36ce720b8c0 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -371,7 +371,14 @@ export class Workspace implements IASTNodeLocation { this.topComments[this.topComments.length - 1].dispose(); } eventUtils.setGroup(existingGroup); - this.variableMap.clear(); + // If this is a flyout workspace, its variable map is shared with the + // parent workspace, so we either don't want to disturb it if we're just + // disposing the flyout, or if the flyout is being disposed because the + // main workspace is being disposed, then the main workspace will handle + // cleaning it up. + if (!this.isFlyout) { + this.variableMap.clear(); + } if (this.potentialVariableMap) { this.potentialVariableMap.clear(); } From e4eb9751cb10ed12c0201c5a8b1a27e2541ed60b Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 31 Oct 2024 11:31:53 -0700 Subject: [PATCH 072/222] fix: improve typings and export additional types (#8631) --- core/blockly.ts | 9 ++++++--- core/clipboard.ts | 4 ++-- core/dropdowndiv.ts | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/core/blockly.ts b/core/blockly.ts index fda49830f5f..7163d10b0c7 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -60,6 +60,7 @@ import { FieldDropdownConfig, FieldDropdownFromJsonConfig, FieldDropdownValidator, + ImageProperties, MenuGenerator, MenuGeneratorFunction, MenuOption, @@ -94,7 +95,7 @@ import { FieldVariableFromJsonConfig, FieldVariableValidator, } from './field_variable.js'; -import {Flyout} from './flyout_base.js'; +import {Flyout, FlyoutItem} from './flyout_base.js'; import {FlyoutButton} from './flyout_button.js'; import {FlyoutSeparator} from './flyout_separator.js'; import {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; @@ -156,7 +157,7 @@ import {IStyleable} from './interfaces/i_styleable.js'; import {IToolbox} from './interfaces/i_toolbox.js'; import {IToolboxItem} from './interfaces/i_toolbox_item.js'; import {IVariableMap} from './interfaces/i_variable_map.js'; -import {IVariableModel} from './interfaces/i_variable_model.js'; +import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import { IVariableBackedParameterModel, isVariableBackedParameterModel, @@ -488,6 +489,7 @@ export { FieldDropdownConfig, FieldDropdownFromJsonConfig, FieldDropdownValidator, + ImageProperties, MenuGenerator, MenuGeneratorFunction, MenuOption, @@ -513,7 +515,7 @@ export { FieldVariableFromJsonConfig, FieldVariableValidator, }; -export {Flyout}; +export {Flyout, FlyoutItem}; export {FlyoutButton}; export {FlyoutMetricsManager}; export {FlyoutSeparator}; @@ -568,6 +570,7 @@ export {IToolbox}; export {IToolboxItem}; export {IVariableMap}; export {IVariableModel}; +export {IVariableState}; export {IVariableBackedParameterModel, isVariableBackedParameterModel}; export {Marker}; export {MarkerManager}; diff --git a/core/clipboard.ts b/core/clipboard.ts index ed574d11287..d63555d1152 100644 --- a/core/clipboard.ts +++ b/core/clipboard.ts @@ -7,7 +7,7 @@ // Former goog.module ID: Blockly.clipboard import type {ICopyData, ICopyable} from './interfaces/i_copyable.js'; -import {BlockPaster} from './clipboard/block_paster.js'; +import {BlockPaster, BlockCopyData} from './clipboard/block_paster.js'; import * as globalRegistry from './registry.js'; import {WorkspaceSvg} from './workspace_svg.js'; import * as registry from './clipboard/registry.js'; @@ -110,4 +110,4 @@ export const TEST_ONLY = { copyInternal, }; -export {BlockPaster, registry}; +export {BlockPaster, BlockCopyData, registry}; diff --git a/core/dropdowndiv.ts b/core/dropdowndiv.ts index 35eb6eaed19..a47e78c2a45 100644 --- a/core/dropdowndiv.ts +++ b/core/dropdowndiv.ts @@ -160,7 +160,7 @@ export function getOwner(): Field | null { * * @returns Div to populate with content. */ -export function getContentDiv(): Element { +export function getContentDiv(): HTMLDivElement { return content; } From 631190c5cb90481a6e5c5f133ce040ea31ccc8de Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 31 Oct 2024 12:02:11 -0700 Subject: [PATCH 073/222] chore: Remove unneeded handling for @suppress and @alias. (#8633) --- tsdoc.json | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tsdoc.json b/tsdoc.json index 51900e7c369..5470830e746 100644 --- a/tsdoc.json +++ b/tsdoc.json @@ -3,10 +3,6 @@ // Include the definitions that are required for API Extractor "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"], "tagDefinitions": [ - { - "tagName": "@alias", - "syntaxKind": "block" - }, { "tagName": "@define", "syntaxKind": "block" @@ -18,18 +14,12 @@ { "tagName": "@nocollapse", "syntaxKind": "modifier" - }, - { - "tagName": "@suppress", - "syntaxKind": "modifier" } ], "supportForTags": { - "@alias": true, "@define": true, "@license": true, - "@nocollapse": true, - "@suppress": true + "@nocollapse": true } } From 2523093cc9b1bc5e44a82c08ab9b534e4e042081 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 7 Nov 2024 12:15:55 -0800 Subject: [PATCH 074/222] refactor!: Remove the InsertionMarkerManager. (#8649) * refactor!: Remove the InsertionMarkerManager. * chore: Remove unused imports. * chore: Remove import of insertion marker manager test. --- core/blockly.ts | 2 - core/insertion_marker_manager.ts | 742 ------------------- core/renderers/common/renderer.ts | 49 -- core/renderers/zelos/renderer.ts | 34 - tests/mocha/index.html | 1 - tests/mocha/insertion_marker_manager_test.js | 443 ----------- 6 files changed, 1271 deletions(-) delete mode 100644 core/insertion_marker_manager.ts delete mode 100644 tests/mocha/insertion_marker_manager_test.js diff --git a/core/blockly.ts b/core/blockly.ts index 7163d10b0c7..aab29b5442c 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -113,7 +113,6 @@ import * as icons from './icons.js'; import {inject} from './inject.js'; import {Input} from './inputs/input.js'; import * as inputs from './inputs.js'; -import {InsertionMarkerManager} from './insertion_marker_manager.js'; import {InsertionMarkerPreviewer} from './insertion_marker_previewer.js'; import {IASTNodeLocation} from './interfaces/i_ast_node_location.js'; import {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; @@ -555,7 +554,6 @@ export {IMetricsManager}; export {IMovable}; export {Input}; export {inputs}; -export {InsertionMarkerManager}; export {InsertionMarkerPreviewer}; export {IObservable, isObservable}; export {IPaster, isPaster}; diff --git a/core/insertion_marker_manager.ts b/core/insertion_marker_manager.ts deleted file mode 100644 index 376297f10e7..00000000000 --- a/core/insertion_marker_manager.ts +++ /dev/null @@ -1,742 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Class that controls updates to connections during drags. - * - * @class - */ -// Former goog.module ID: Blockly.InsertionMarkerManager - -import {finishQueuedRenders} from './render_management.js'; -import * as blockAnimations from './block_animations.js'; -import type {BlockSvg} from './block_svg.js'; -import * as common from './common.js'; -import {ComponentManager} from './component_manager.js'; -import {config} from './config.js'; -import * as blocks from './serialization/blocks.js'; -import * as eventUtils from './events/utils.js'; -import type {IDeleteArea} from './interfaces/i_delete_area.js'; -import type {IDragTarget} from './interfaces/i_drag_target.js'; -import type {RenderedConnection} from './rendered_connection.js'; -import type {Coordinate} from './utils/coordinate.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; -import * as renderManagement from './render_management.js'; - -/** Represents a nearby valid connection. */ -interface CandidateConnection { - /** - * A nearby valid connection that is compatible with local. - * This is not on any of the blocks that are being dragged. - */ - closest: RenderedConnection; - /** - * A connection on the dragging stack that is compatible with closest. This is - * on the top block that is being dragged or the last block in the dragging - * stack. - */ - local: RenderedConnection; - radius: number; -} - -/** - * Class that controls updates to connections during drags. It is primarily - * responsible for finding the closest eligible connection and highlighting or - * unhighlighting it as needed during a drag. - * - * @deprecated v10 - Use an IConnectionPreviewer instead. - */ -export class InsertionMarkerManager { - /** - * The top block in the stack being dragged. - * Does not change during a drag. - */ - private readonly topBlock: BlockSvg; - - /** - * The workspace on which these connections are being dragged. - * Does not change during a drag. - */ - private readonly workspace: WorkspaceSvg; - - /** - * The last connection on the stack, if it's not the last connection on the - * first block. - * Set in initAvailableConnections, if at all. - */ - private lastOnStack: RenderedConnection | null = null; - - /** - * The insertion marker corresponding to the last block in the stack, if - * that's not the same as the first block in the stack. - * Set in initAvailableConnections, if at all - */ - private lastMarker: BlockSvg | null = null; - - /** - * The insertion marker that shows up between blocks to show where a block - * would go if dropped immediately. - */ - private firstMarker: BlockSvg; - - /** - * Information about the connection that would be made if the dragging block - * were released immediately. Updated on every mouse move. - */ - private activeCandidate: CandidateConnection | null = null; - - /** - * Whether the block would be deleted if it were dropped immediately. - * Updated on every mouse move. - * - * @internal - */ - public wouldDeleteBlock = false; - - /** - * Connection on the insertion marker block that corresponds to - * the active candidate's local connection on the currently dragged block. - */ - private markerConnection: RenderedConnection | null = null; - - /** The block that currently has an input being highlighted, or null. */ - private highlightedBlock: BlockSvg | null = null; - - /** The block being faded to indicate replacement, or null. */ - private fadedBlock: BlockSvg | null = null; - - /** - * The connections on the dragging blocks that are available to connect to - * other blocks. This includes all open connections on the top block, as - * well as the last connection on the block stack. - */ - private availableConnections: RenderedConnection[]; - - /** @param block The top block in the stack being dragged. */ - constructor(block: BlockSvg) { - common.setSelected(block); - this.topBlock = block; - - this.workspace = block.workspace; - - this.firstMarker = this.createMarkerBlock(this.topBlock); - - this.availableConnections = this.initAvailableConnections(); - - if (this.lastOnStack) { - this.lastMarker = this.createMarkerBlock( - this.lastOnStack.getSourceBlock(), - ); - } - } - - /** - * Sever all links from this object. - * - * @internal - */ - dispose() { - this.availableConnections.length = 0; - this.disposeInsertionMarker(this.firstMarker); - this.disposeInsertionMarker(this.lastMarker); - } - - /** - * Update the available connections for the top block. These connections can - * change if a block is unplugged and the stack is healed. - * - * @internal - */ - updateAvailableConnections() { - this.availableConnections = this.initAvailableConnections(); - } - - /** - * Return whether the block would be connected if dropped immediately, based - * on information from the most recent move event. - * - * @returns True if the block would be connected if dropped immediately. - * @internal - */ - wouldConnectBlock(): boolean { - return !!this.activeCandidate; - } - - /** - * Connect to the closest connection and render the results. - * This should be called at the end of a drag. - * - * @internal - */ - applyConnections() { - if (!this.activeCandidate) return; - eventUtils.disable(); - this.hidePreview(); - eventUtils.enable(); - const {local, closest} = this.activeCandidate; - local.connect(closest); - const inferiorConnection = local.isSuperior() ? closest : local; - const rootBlock = this.topBlock.getRootBlock(); - - finishQueuedRenders().then(() => { - blockAnimations.connectionUiEffect(inferiorConnection.getSourceBlock()); - // bringToFront is incredibly expensive. Delay until the next frame. - setTimeout(() => { - rootBlock.bringToFront(); - }, 0); - }); - } - - /** - * Update connections based on the most recent move location. - * - * @param dxy Position relative to drag start, in workspace units. - * @param dragTarget The drag target that the block is currently over. - * @internal - */ - update(dxy: Coordinate, dragTarget: IDragTarget | null) { - const newCandidate = this.getCandidate(dxy); - - this.wouldDeleteBlock = this.shouldDelete(!!newCandidate, dragTarget); - - const shouldUpdate = - this.wouldDeleteBlock || this.shouldUpdatePreviews(newCandidate, dxy); - - if (shouldUpdate) { - // Don't fire events for insertion marker creation or movement. - eventUtils.disable(); - this.maybeHidePreview(newCandidate); - this.maybeShowPreview(newCandidate); - eventUtils.enable(); - } - } - - /** - * Create an insertion marker that represents the given block. - * - * @param sourceBlock The block that the insertion marker will represent. - * @returns The insertion marker that represents the given block. - */ - private createMarkerBlock(sourceBlock: BlockSvg): BlockSvg { - eventUtils.disable(); - let result: BlockSvg; - try { - const blockJson = blocks.save(sourceBlock, { - addCoordinates: false, - addInputBlocks: false, - addNextBlocks: false, - doFullSerialization: false, - }); - - if (!blockJson) { - throw new Error( - `Failed to serialize source block. ${sourceBlock.toDevString()}`, - ); - } - - result = blocks.append(blockJson, this.workspace) as BlockSvg; - - // Turn shadow blocks that are created programmatically during - // initalization to insertion markers too. - for (const block of result.getDescendants(false)) { - block.setInsertionMarker(true); - } - - result.initSvg(); - result.getSvgRoot().setAttribute('visibility', 'hidden'); - } finally { - eventUtils.enable(); - } - - return result; - } - - /** - * Populate the list of available connections on this block stack. If the - * stack has more than one block, this function will also update lastOnStack. - * - * @returns A list of available connections. - */ - private initAvailableConnections(): RenderedConnection[] { - const available = this.topBlock.getConnections_(false); - // Also check the last connection on this stack - const lastOnStack = this.topBlock.lastConnectionInStack(true); - if (lastOnStack && lastOnStack !== this.topBlock.nextConnection) { - available.push(lastOnStack); - this.lastOnStack = lastOnStack; - } - return available; - } - - /** - * Whether the previews (insertion marker and replacement marker) should be - * updated based on the closest candidate and the current drag distance. - * - * @param newCandidate A new candidate connection that may replace the current - * best candidate. - * @param dxy Position relative to drag start, in workspace units. - * @returns Whether the preview should be updated. - */ - private shouldUpdatePreviews( - newCandidate: CandidateConnection | null, - dxy: Coordinate, - ): boolean { - // Only need to update if we were showing a preview before. - if (!newCandidate) return !!this.activeCandidate; - - // We weren't showing a preview before, but we should now. - if (!this.activeCandidate) return true; - - // We're already showing an insertion marker. - // Decide whether the new connection has higher priority. - const {local: activeLocal, closest: activeClosest} = this.activeCandidate; - if ( - activeClosest === newCandidate.closest && - activeLocal === newCandidate.local - ) { - // The connection was the same as the current connection. - return false; - } - - const xDiff = activeLocal.x + dxy.x - activeClosest.x; - const yDiff = activeLocal.y + dxy.y - activeClosest.y; - const curDistance = Math.sqrt(xDiff * xDiff + yDiff * yDiff); - // Slightly prefer the existing preview over a new preview. - return ( - newCandidate.radius < curDistance - config.currentConnectionPreference - ); - } - - /** - * Find the nearest valid connection, which may be the same as the current - * closest connection. - * - * @param dxy Position relative to drag start, in workspace units. - * @returns An object containing a local connection, a closest connection, and - * a radius. - */ - private getCandidate(dxy: Coordinate): CandidateConnection | null { - // It's possible that a block has added or removed connections during a - // drag, (e.g. in a drag/move event handler), so let's update the available - // connections. Note that this will be called on every move while dragging, - // so it might cause slowness, especially if the block stack is large. If - // so, maybe it could be made more efficient. Also note that we won't update - // the connections if we've already connected the insertion marker to a - // block. - if (!this.markerConnection || !this.markerConnection.isConnected()) { - this.updateAvailableConnections(); - } - - let radius = this.getStartRadius(); - let candidate = null; - for (let i = 0; i < this.availableConnections.length; i++) { - const myConnection = this.availableConnections[i]; - const neighbour = myConnection.closest(radius, dxy); - if (neighbour.connection) { - candidate = { - closest: neighbour.connection, - local: myConnection, - radius: neighbour.radius, - }; - radius = neighbour.radius; - } - } - return candidate; - } - - /** - * Decide the radius at which to start searching for the closest connection. - * - * @returns The radius at which to start the search for the closest - * connection. - */ - private getStartRadius(): number { - // If there is already a connection highlighted, - // increase the radius we check for making new connections. - // When a connection is highlighted, blocks move around when the - // insertion marker is created, which could cause the connection became out - // of range. By increasing radiusConnection when a connection already - // exists, we never "lose" the connection from the offset. - return this.activeCandidate - ? config.connectingSnapRadius - : config.snapRadius; - } - - /** - * Whether ending the drag would delete the block. - * - * @param newCandidate Whether there is a candidate connection that the - * block could connect to if the drag ended immediately. - * @param dragTarget The drag target that the block is currently over. - * @returns Whether dropping the block immediately would delete the block. - */ - private shouldDelete( - newCandidate: boolean, - dragTarget: IDragTarget | null, - ): boolean { - if (dragTarget) { - const componentManager = this.workspace.getComponentManager(); - const isDeleteArea = componentManager.hasCapability( - dragTarget.id, - ComponentManager.Capability.DELETE_AREA, - ); - if (isDeleteArea) { - return (dragTarget as IDeleteArea).wouldDelete(this.topBlock); - } - } - return false; - } - - /** - * Show an insertion marker or replacement highlighting during a drag, if - * needed. - * At the beginning of this function, this.activeConnection should be null. - * - * @param newCandidate A new candidate connection that may replace the current - * best candidate. - */ - private maybeShowPreview(newCandidate: CandidateConnection | null) { - if (this.wouldDeleteBlock) return; // Nope, don't add a marker. - if (!newCandidate) return; // Nothing to connect to. - - const closest = newCandidate.closest; - - // Something went wrong and we're trying to connect to an invalid - // connection. - if ( - closest === this.activeCandidate?.closest || - closest.getSourceBlock().isInsertionMarker() - ) { - console.log('Trying to connect to an insertion marker'); - return; - } - this.activeCandidate = newCandidate; - // Add an insertion marker or replacement marker. - this.showPreview(this.activeCandidate); - } - - /** - * A preview should be shown. This function figures out if it should be a - * block highlight or an insertion marker, and shows the appropriate one. - * - * @param activeCandidate The connection that will be made if the drag ends - * immediately. - */ - private showPreview(activeCandidate: CandidateConnection) { - const renderer = this.workspace.getRenderer(); - const method = renderer.getConnectionPreviewMethod( - activeCandidate.closest, - activeCandidate.local, - this.topBlock, - ); - - switch (method) { - case InsertionMarkerManager.PREVIEW_TYPE.INPUT_OUTLINE: - this.showInsertionInputOutline(activeCandidate); - break; - case InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER: - this.showInsertionMarker(activeCandidate); - break; - case InsertionMarkerManager.PREVIEW_TYPE.REPLACEMENT_FADE: - this.showReplacementFade(activeCandidate); - break; - } - - // Optionally highlight the actual connection, as a nod to previous - // behaviour. - if (renderer.shouldHighlightConnection(activeCandidate.closest)) { - activeCandidate.closest.highlight(); - } - } - - /** - * Hide an insertion marker or replacement highlighting during a drag, if - * needed. - * At the end of this function, this.activeCandidate will be null. - * - * @param newCandidate A new candidate connection that may replace the current - * best candidate. - */ - private maybeHidePreview(newCandidate: CandidateConnection | null) { - // If there's no new preview, remove the old one but don't bother deleting - // it. We might need it later, and this saves disposing of it and recreating - // it. - if (!newCandidate) { - this.hidePreview(); - } else { - if (this.activeCandidate) { - const closestChanged = - this.activeCandidate.closest !== newCandidate.closest; - const localChanged = this.activeCandidate.local !== newCandidate.local; - - // If there's a new preview and there was a preview before, and either - // connection has changed, remove the old preview. - // Also hide if we had a preview before but now we're going to delete - // instead. - if (closestChanged || localChanged || this.wouldDeleteBlock) { - this.hidePreview(); - } - } - } - - // Either way, clear out old state. - this.markerConnection = null; - this.activeCandidate = null; - } - - /** - * A preview should be hidden. Loop through all possible preview modes - * and hide everything. - */ - private hidePreview() { - const closest = this.activeCandidate?.closest; - if ( - closest && - closest.targetBlock() && - this.workspace.getRenderer().shouldHighlightConnection(closest) - ) { - closest.unhighlight(); - } - this.hideReplacementFade(); - this.hideInsertionInputOutline(); - this.hideInsertionMarker(); - } - - /** - * Shows an insertion marker connected to the appropriate blocks (based on - * manager state). - * - * @param activeCandidate The connection that will be made if the drag ends - * immediately. - */ - private showInsertionMarker(activeCandidate: CandidateConnection) { - const {local, closest} = activeCandidate; - - const isLastInStack = this.lastOnStack && local === this.lastOnStack; - let insertionMarker = isLastInStack ? this.lastMarker : this.firstMarker; - if (!insertionMarker) { - throw new Error( - 'Cannot show the insertion marker because there is no insertion ' + - 'marker block', - ); - } - let imConn; - try { - imConn = insertionMarker.getMatchingConnection( - local.getSourceBlock(), - local, - ); - } catch (e) { - // It's possible that the number of connections on the local block has - // changed since the insertion marker was originally created. Let's - // recreate the insertion marker and try again. In theory we could - // probably recreate the marker block (e.g. in getCandidate_), which is - // called more often during the drag, but creating a block that often - // might be too slow, so we only do it if necessary. - if (isLastInStack && this.lastOnStack) { - this.disposeInsertionMarker(this.lastMarker); - this.lastMarker = this.createMarkerBlock( - this.lastOnStack.getSourceBlock(), - ); - insertionMarker = this.lastMarker; - } else { - this.disposeInsertionMarker(this.firstMarker); - this.firstMarker = this.createMarkerBlock(this.topBlock); - insertionMarker = this.firstMarker; - } - - if (!insertionMarker) { - throw new Error( - 'Cannot show the insertion marker because there is no insertion ' + - 'marker block', - ); - } - imConn = insertionMarker.getMatchingConnection( - local.getSourceBlock(), - local, - ); - } - - if (!imConn) { - throw new Error( - 'Cannot show the insertion marker because there is no ' + - 'associated connection', - ); - } - - if (imConn === this.markerConnection) { - throw new Error( - "Made it to showInsertionMarker_ even though the marker isn't " + - 'changing', - ); - } - - // Render disconnected from everything else so that we have a valid - // connection location. - insertionMarker.queueRender(); - renderManagement.triggerQueuedRenders(); - - // Connect() also renders the insertion marker. - imConn.connect(closest); - - const originalOffsetToTarget = { - x: closest.x - imConn.x, - y: closest.y - imConn.y, - }; - const originalOffsetInBlock = imConn.getOffsetInBlock().clone(); - const imConnConst = imConn; - renderManagement.finishQueuedRenders().then(() => { - // Position so that the existing block doesn't move. - insertionMarker?.positionNearConnection( - imConnConst, - originalOffsetToTarget, - originalOffsetInBlock, - ); - insertionMarker?.getSvgRoot().setAttribute('visibility', 'visible'); - }); - - this.markerConnection = imConn; - } - - /** - * Disconnects and hides the current insertion marker. Should return the - * blocks to their original state. - */ - private hideInsertionMarker() { - if (!this.markerConnection) return; - - const markerConn = this.markerConnection; - const imBlock = markerConn.getSourceBlock(); - const markerPrev = imBlock.previousConnection; - const markerOutput = imBlock.outputConnection; - - if (!markerPrev?.targetConnection && !markerOutput?.targetConnection) { - // If we are the top block, unplugging doesn't do anything. - // The marker connection may not have a target block if we are hiding - // as part of applying connections. - markerConn.targetBlock()?.unplug(false); - } else { - imBlock.unplug(true); - } - - if (markerConn.targetConnection) { - throw Error( - 'markerConnection still connected at the end of ' + - 'disconnectInsertionMarker', - ); - } - - this.markerConnection = null; - const svg = imBlock.getSvgRoot(); - if (svg) { - svg.setAttribute('visibility', 'hidden'); - } - } - - /** - * Shows an outline around the input the closest connection belongs to. - * - * @param activeCandidate The connection that will be made if the drag ends - * immediately. - */ - private showInsertionInputOutline(activeCandidate: CandidateConnection) { - const closest = activeCandidate.closest; - this.highlightedBlock = closest.getSourceBlock(); - this.highlightedBlock.highlightShapeForInput(closest, true); - } - - /** Hides any visible input outlines. */ - private hideInsertionInputOutline() { - if (!this.highlightedBlock) return; - - if (!this.activeCandidate) { - throw new Error( - 'Cannot hide the insertion marker outline because ' + - 'there is no active candidate', - ); - } - this.highlightedBlock.highlightShapeForInput( - this.activeCandidate.closest, - false, - ); - this.highlightedBlock = null; - } - - /** - * Shows a replacement fade affect on the closest connection's target block - * (the block that is currently connected to it). - * - * @param activeCandidate The connection that will be made if the drag ends - * immediately. - */ - private showReplacementFade(activeCandidate: CandidateConnection) { - this.fadedBlock = activeCandidate.closest.targetBlock(); - if (!this.fadedBlock) { - throw new Error( - 'Cannot show the replacement fade because the ' + - 'closest connection does not have a target block', - ); - } - this.fadedBlock.fadeForReplacement(true); - } - - /** - * Hides/Removes any visible fade affects. - */ - private hideReplacementFade() { - if (!this.fadedBlock) return; - - this.fadedBlock.fadeForReplacement(false); - this.fadedBlock = null; - } - - /** - * Get a list of the insertion markers that currently exist. Drags have 0, 1, - * or 2 insertion markers. - * - * @returns A possibly empty list of insertion marker blocks. - * @internal - */ - getInsertionMarkers(): BlockSvg[] { - const result = []; - if (this.firstMarker) { - result.push(this.firstMarker); - } - if (this.lastMarker) { - result.push(this.lastMarker); - } - return result; - } - - /** - * Safely disposes of an insertion marker. - */ - private disposeInsertionMarker(marker: BlockSvg | null) { - if (marker) { - eventUtils.disable(); - try { - marker.dispose(); - } finally { - eventUtils.enable(); - } - } - } -} - -export namespace InsertionMarkerManager { - /** - * An enum describing different kinds of previews the InsertionMarkerManager - * could display. - */ - export enum PREVIEW_TYPE { - INSERTION_MARKER = 0, - INPUT_OUTLINE = 1, - REPLACEMENT_FADE = 2, - } -} - -export type PreviewType = InsertionMarkerManager.PREVIEW_TYPE; -export const PreviewType = InsertionMarkerManager.PREVIEW_TYPE; diff --git a/core/renderers/common/renderer.ts b/core/renderers/common/renderer.ts index 15a958db463..255bc81ff52 100644 --- a/core/renderers/common/renderer.ts +++ b/core/renderers/common/renderer.ts @@ -10,13 +10,8 @@ import type {Block} from '../../block.js'; import type {BlockSvg} from '../../block_svg.js'; import {Connection} from '../../connection.js'; import {ConnectionType} from '../../connection_type.js'; -import { - InsertionMarkerManager, - PreviewType, -} from '../../insertion_marker_manager.js'; import type {IRegistrable} from '../../interfaces/i_registrable.js'; import type {Marker} from '../../keyboard_nav/marker.js'; -import type {RenderedConnection} from '../../rendered_connection.js'; import type {BlockStyle, Theme} from '../../theme.js'; import type {WorkspaceSvg} from '../../workspace_svg.js'; @@ -26,7 +21,6 @@ import type {IPathObject} from './i_path_object.js'; import {RenderInfo} from './info.js'; import {MarkerSvg} from './marker_svg.js'; import {PathObject} from './path_object.js'; -import * as deprecation from '../../utils/deprecation.js'; /** * The base class for a block renderer. @@ -224,49 +218,6 @@ export class Renderer implements IRegistrable { ); } - /** - * Chooses a connection preview method based on the available connection, the - * current dragged connection, and the block being dragged. - * - * @param closest The available connection. - * @param local The connection currently being dragged. - * @param topBlock The block currently being dragged. - * @returns The preview type to display. - * - * @deprecated v10 - This function is no longer respected. A custom - * IConnectionPreviewer may be able to fulfill the functionality. - */ - getConnectionPreviewMethod( - closest: RenderedConnection, - local: RenderedConnection, - topBlock: BlockSvg, - ): PreviewType { - deprecation.warn( - 'getConnectionPreviewMethod', - 'v10', - 'v12', - 'an IConnectionPreviewer, if it fulfills your use case.', - ); - if ( - local.type === ConnectionType.OUTPUT_VALUE || - local.type === ConnectionType.PREVIOUS_STATEMENT - ) { - if ( - !closest.isConnected() || - this.orphanCanConnectAtEnd( - topBlock, - closest.targetBlock() as BlockSvg, - local.type, - ) - ) { - return InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER; - } - return InsertionMarkerManager.PREVIEW_TYPE.REPLACEMENT_FADE; - } - - return InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER; - } - /** * Render the block. * diff --git a/core/renderers/zelos/renderer.ts b/core/renderers/zelos/renderer.ts index 354a3f35adf..44fddacb295 100644 --- a/core/renderers/zelos/renderer.ts +++ b/core/renderers/zelos/renderer.ts @@ -7,10 +7,7 @@ // Former goog.module ID: Blockly.zelos.Renderer import type {BlockSvg} from '../../block_svg.js'; -import {ConnectionType} from '../../connection_type.js'; -import {InsertionMarkerManager} from '../../insertion_marker_manager.js'; import type {Marker} from '../../keyboard_nav/marker.js'; -import type {RenderedConnection} from '../../rendered_connection.js'; import type {BlockStyle} from '../../theme.js'; import type {WorkspaceSvg} from '../../workspace_svg.js'; import * as blockRendering from '../common/block_rendering.js'; @@ -22,7 +19,6 @@ import {Drawer} from './drawer.js'; import {RenderInfo} from './info.js'; import {MarkerSvg} from './marker_svg.js'; import {PathObject} from './path_object.js'; -import * as deprecation from '../../utils/deprecation.js'; /** * The zelos renderer. This renderer emulates Scratch-style and MakeCode-style @@ -108,36 +104,6 @@ export class Renderer extends BaseRenderer { override getConstants(): ConstantProvider { return this.constants_; } - - /** - * @deprecated v10 - This function is no longer respected. A custom - * IConnectionPreviewer may be able to fulfill the functionality. - */ - override getConnectionPreviewMethod( - closest: RenderedConnection, - local: RenderedConnection, - topBlock: BlockSvg, - ) { - deprecation.warn( - 'getConnectionPreviewMethod', - 'v10', - 'v12', - 'an IConnectionPreviewer, if it fulfills your use case.', - ); - if (local.type === ConnectionType.OUTPUT_VALUE) { - if (!closest.isConnected()) { - return InsertionMarkerManager.PREVIEW_TYPE.INPUT_OUTLINE; - } - // TODO: Returning this is a total hack, because we don't want to show - // a replacement fade, we want to show an outline affect. - // Sadly zelos does not support showing an outline around filled - // inputs, so we have to pretend like the connected block is getting - // replaced. - return InsertionMarkerManager.PREVIEW_TYPE.REPLACEMENT_FADE; - } - - return super.getConnectionPreviewMethod(closest, local, topBlock); - } } blockRendering.register('zelos', Renderer); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 58a71e0acdc..ce9948a2127 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -95,7 +95,6 @@ import './icon_test.js'; import './input_test.js'; import './insertion_marker_test.js'; - import './insertion_marker_manager_test.js'; import './jso_deserialization_test.js'; import './jso_serialization_test.js'; import './json_test.js'; diff --git a/tests/mocha/insertion_marker_manager_test.js b/tests/mocha/insertion_marker_manager_test.js deleted file mode 100644 index e6992109fa7..00000000000 --- a/tests/mocha/insertion_marker_manager_test.js +++ /dev/null @@ -1,443 +0,0 @@ -/** - * @license - * Copyright 2022 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import {assert} from '../../node_modules/chai/chai.js'; -import { - sharedTestSetup, - sharedTestTeardown, -} from './test_helpers/setup_teardown.js'; -import { - defineRowBlock, - defineRowToStackBlock, - defineStackBlock, -} from './test_helpers/block_definitions.js'; - -suite('Insertion marker manager', function () { - setup(function () { - sharedTestSetup.call(this); - defineRowBlock(); - defineStackBlock(); - defineRowToStackBlock(); - this.workspace = Blockly.inject('blocklyDiv'); - }); - teardown(function () { - sharedTestTeardown.call(this); - }); - - suite('Creating markers', function () { - function createBlocksAndManager(workspace, state) { - Blockly.serialization.workspaces.load(state, workspace); - const block = workspace.getBlockById('first'); - const manager = new Blockly.InsertionMarkerManager(block); - return manager; - } - - test('One stack block creates one marker', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'stack_block', - 'id': 'first', - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 1); - }); - - test('Two stack blocks create two markers', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'stack_block', - 'id': 'first', - 'next': { - 'block': { - 'type': 'stack_block', - 'id': 'second', - }, - }, - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 2); - }); - - test('Three stack blocks create two markers', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'stack_block', - 'id': 'first', - 'next': { - 'block': { - 'type': 'stack_block', - 'id': 'second', - 'next': { - 'block': { - 'type': 'stack_block', - 'id': 'third', - }, - }, - }, - }, - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 2); - }); - - test('One value block creates one marker', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'row_block', - 'id': 'first', - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 1); - }); - - test('Two value blocks create one marker', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'row_block', - 'id': 'first', - 'inputs': { - 'INPUT': { - 'block': { - 'type': 'row_block', - 'id': 'second', - }, - }, - }, - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 1); - }); - - test('One row to stack block creates one marker', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'row_to_stack_block', - 'id': 'first', - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 1); - }); - - test('Row to stack block with child creates two markers', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'row_to_stack_block', - 'id': 'first', - 'next': { - 'block': { - 'type': 'stack_block', - 'id': 'second', - }, - }, - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 2); - }); - - suite('children being set as insertion markers', function () { - setup(function () { - Blockly.Blocks['shadows_in_init'] = { - init: function () { - this.appendValueInput('test').connection.setShadowState({ - 'type': 'math_number', - }); - this.setPreviousStatement(true); - }, - }; - - Blockly.Blocks['shadows_in_load'] = { - init: function () { - this.appendValueInput('test'); - this.setPreviousStatement(true); - }, - - loadExtraState: function () { - this.getInput('test').connection.setShadowState({ - 'type': 'math_number', - }); - }, - - saveExtraState: function () { - return true; - }, - }; - }); - - teardown(function () { - delete Blockly.Blocks['shadows_in_init']; - delete Blockly.Blocks['shadows_in_load']; - }); - - test('Shadows added in init are set as insertion markers', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'id': 'first', - 'type': 'shadows_in_init', - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.isTrue( - markers[0].getChildren()[0].isInsertionMarker(), - 'Expected the shadow block to be an insertion maker', - ); - }); - - test('Shadows added in `loadExtraState` are set as insertion markers', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'id': 'first', - 'type': 'shadows_in_load', - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.isTrue( - markers[0].getChildren()[0].isInsertionMarker(), - 'Expected the shadow block to be an insertion maker', - ); - }); - }); - }); - - suite('Would delete block', function () { - setup(function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'stack_block', - 'id': 'first', - }, - ], - }, - }; - Blockly.serialization.workspaces.load(state, this.workspace); - this.block = this.workspace.getBlockById('first'); - this.manager = new Blockly.InsertionMarkerManager(this.block); - - const componentManager = this.workspace.getComponentManager(); - this.stub = sinon.stub(componentManager, 'hasCapability'); - this.dxy = new Blockly.utils.Coordinate(0, 0); - }); - - test('Over delete area and accepted would delete', function () { - this.stub - .withArgs( - 'fakeDragTarget', - Blockly.ComponentManager.Capability.DELETE_AREA, - ) - .returns(true); - const fakeDragTarget = { - wouldDelete: sinon.fake.returns(true), - id: 'fakeDragTarget', - }; - this.manager.update(this.dxy, fakeDragTarget); - assert.isTrue(this.manager.wouldDeleteBlock); - }); - - test('Over delete area and rejected would not delete', function () { - this.stub - .withArgs( - 'fakeDragTarget', - Blockly.ComponentManager.Capability.DELETE_AREA, - ) - .returns(true); - const fakeDragTarget = { - wouldDelete: sinon.fake.returns(false), - id: 'fakeDragTarget', - }; - this.manager.update(this.dxy, fakeDragTarget); - assert.isFalse(this.manager.wouldDeleteBlock); - }); - - test('Drag target is not a delete area would not delete', function () { - this.stub - .withArgs( - 'fakeDragTarget', - Blockly.ComponentManager.Capability.DELETE_AREA, - ) - .returns(false); - const fakeDragTarget = { - wouldDelete: sinon.fake.returns(false), - id: 'fakeDragTarget', - }; - this.manager.update(this.dxy, fakeDragTarget); - assert.isFalse(this.manager.wouldDeleteBlock); - }); - - test('Not over drag target would not delete', function () { - this.manager.update(this.dxy, null); - assert.isFalse(this.manager.wouldDeleteBlock); - }); - }); - - suite('Would connect stack blocks', function () { - setup(function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'stack_block', - 'id': 'first', - 'x': 0, - 'y': 0, - }, - { - 'type': 'stack_block', - 'id': 'other', - 'x': 200, - 'y': 200, - }, - ], - }, - }; - Blockly.serialization.workspaces.load(state, this.workspace); - this.block = this.workspace.getBlockById('first'); - this.block.setDragging(true); - this.manager = new Blockly.InsertionMarkerManager(this.block); - }); - - test('No other blocks nearby would not connect', function () { - this.manager.update(new Blockly.utils.Coordinate(0, 0), null); - assert.isFalse(this.manager.wouldConnectBlock()); - }); - - test('Near other block and above would connect before', function () { - this.manager.update(new Blockly.utils.Coordinate(200, 190), null); - assert.isTrue(this.manager.wouldConnectBlock()); - const markers = this.manager.getInsertionMarkers(); - assert.equal(markers.length, 1); - const marker = markers[0]; - assert.isTrue(marker.nextConnection.isConnected()); - }); - - test('Near other block and below would connect after', function () { - this.manager.update(new Blockly.utils.Coordinate(200, 210), null); - assert.isTrue(this.manager.wouldConnectBlock()); - const markers = this.manager.getInsertionMarkers(); - assert.equal(markers.length, 1); - const marker = markers[0]; - assert.isTrue(marker.previousConnection.isConnected()); - }); - - test('Near other block and left would connect', function () { - this.manager.update(new Blockly.utils.Coordinate(190, 200), null); - assert.isTrue(this.manager.wouldConnectBlock()); - }); - - test('Near other block and right would connect', function () { - this.manager.update(new Blockly.utils.Coordinate(210, 200), null); - assert.isTrue(this.manager.wouldConnectBlock()); - }); - }); - - suite('Would connect row blocks', function () { - setup(function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'row_block', - 'id': 'first', - 'x': 0, - 'y': 0, - }, - { - 'type': 'row_block', - 'id': 'other', - 'x': 200, - 'y': 200, - }, - ], - }, - }; - Blockly.serialization.workspaces.load(state, this.workspace); - this.block = this.workspace.getBlockById('first'); - this.block.setDragging(true); - this.manager = new Blockly.InsertionMarkerManager(this.block); - }); - - test('No other blocks nearby would not connect', function () { - this.manager.update(new Blockly.utils.Coordinate(0, 0), null); - assert.isFalse(this.manager.wouldConnectBlock()); - }); - - test('Near other block and above would connect', function () { - this.manager.update(new Blockly.utils.Coordinate(200, 190), null); - assert.isTrue(this.manager.wouldConnectBlock()); - }); - - test('Near other block and below would connect', function () { - this.manager.update(new Blockly.utils.Coordinate(200, 210), null); - assert.isTrue(this.manager.wouldConnectBlock()); - }); - - test('Near other block and left would connect before', function () { - this.manager.update(new Blockly.utils.Coordinate(190, 200), null); - assert.isTrue(this.manager.wouldConnectBlock()); - const markers = this.manager.getInsertionMarkers(); - assert.isTrue(markers[0].getInput('INPUT').connection.isConnected()); - }); - - test('Near other block and right would connect after', function () { - this.manager.update(new Blockly.utils.Coordinate(210, 200), null); - assert.isTrue(this.manager.wouldConnectBlock()); - const markers = this.manager.getInsertionMarkers(); - assert.isTrue(markers[0].outputConnection.isConnected()); - }); - }); -}); From d804c1a3c40c56bdcb342da069e04b4979f46d0f Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 7 Nov 2024 12:16:17 -0800 Subject: [PATCH 075/222] refactor!: Improve ability to use CSS to style Blockly. (#8647) * refactor!: Rename blocklyTreeIconClosed to blocklyToolboxCategoryIconClosed. * refactor!: Rename blocklyTreeLabel to blocklyToolboxCategoryLabel * refactor!: Rename blocklyToolboxDiv to blocklyToolbox. * refactor: remove unreferenced CSS classes. * refactor!: Remove the blocklyArrowTop and blocklyArrowBottom classes. * feat: Add a blocklyTextInputField class to text fields. --- core/css.ts | 14 -------------- core/dropdowndiv.ts | 17 +++++------------ core/field_textinput.ts | 8 ++++++++ core/toolbox/category.ts | 24 ++++++++++++------------ core/toolbox/separator.ts | 2 +- core/toolbox/toolbox.ts | 4 ++-- tests/mocha/toolbox_test.js | 2 +- 7 files changed, 29 insertions(+), 42 deletions(-) diff --git a/core/css.ts b/core/css.ts index d18d930a943..22abfa32a22 100644 --- a/core/css.ts +++ b/core/css.ts @@ -132,22 +132,12 @@ let content = ` z-index: -1; background-color: inherit; border-color: inherit; -} - -.blocklyArrowTop { border-top: 1px solid; border-left: 1px solid; border-top-left-radius: 4px; border-color: inherit; } -.blocklyArrowBottom { - border-bottom: 1px solid; - border-right: 1px solid; - border-bottom-right-radius: 4px; - border-color: inherit; -} - .blocklyHighlightedConnectionPath { fill: none; stroke: #fc3; @@ -243,10 +233,6 @@ let content = ` cursor: inherit; } -.blocklyFieldDropdown:not(.blocklyHidden) { - display: block; -} - .blocklyIconGroup { cursor: default; } diff --git a/core/dropdowndiv.ts b/core/dropdowndiv.ts index a47e78c2a45..5ae1b99cffe 100644 --- a/core/dropdowndiv.ts +++ b/core/dropdowndiv.ts @@ -697,19 +697,12 @@ function positionInternal( // Update arrow CSS. if (metrics.arrowVisible) { + const x = metrics.arrowX; + const y = metrics.arrowY; + const rotation = metrics.arrowAtTop ? 45 : 225; arrow.style.display = ''; - arrow.style.transform = - 'translate(' + - metrics.arrowX + - 'px,' + - metrics.arrowY + - 'px) rotate(45deg)'; - arrow.setAttribute( - 'class', - metrics.arrowAtTop - ? 'blocklyDropDownArrow blocklyArrowTop' - : 'blocklyDropDownArrow blocklyArrowBottom', - ); + arrow.style.transform = `translate(${x}px, ${y}px) rotate(${rotation}deg)`; + arrow.setAttribute('class', 'blocklyDropDownArrow'); } else { arrow.style.display = 'none'; } diff --git a/core/field_textinput.ts b/core/field_textinput.ts index 39bdca97056..5b754624aff 100644 --- a/core/field_textinput.ts +++ b/core/field_textinput.ts @@ -22,6 +22,7 @@ import { } from './field_input.js'; import * as fieldRegistry from './field_registry.js'; import * as parsing from './utils/parsing.js'; +import * as dom from './utils/dom.js'; /** * Class for an editable text field. @@ -49,6 +50,13 @@ export class FieldTextInput extends FieldInput { super(value, validator, config); } + override initView() { + super.initView(); + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyTextInputField'); + } + } + /** * Ensure that the input value casts to a valid string. * diff --git a/core/toolbox/category.ts b/core/toolbox/category.ts index 06f219e5eb8..c173c33ca03 100644 --- a/core/toolbox/category.ts +++ b/core/toolbox/category.ts @@ -135,11 +135,11 @@ export class ToolboxCategory 'row': 'blocklyToolboxCategory', 'rowcontentcontainer': 'blocklyTreeRowContentContainer', 'icon': 'blocklyToolboxCategoryIcon', - 'label': 'blocklyTreeLabel', + 'label': 'blocklyToolboxCategoryLabel', 'contents': 'blocklyToolboxCategoryGroup', 'selected': 'blocklyToolboxSelected', 'openicon': 'blocklyToolboxCategoryIconOpen', - 'closedicon': 'blocklyTreeIconClosed', + 'closedicon': 'blocklyToolboxCategoryIconClosed', }; } @@ -663,11 +663,11 @@ Css.register(` background-color: rgba(255, 255, 255, .2); } -.blocklyToolboxDiv[layout="h"] .blocklyToolboxCategoryContainer { +.blocklyToolbox[layout="h"] .blocklyToolboxCategoryContainer { margin: 1px 5px 1px 0; } -.blocklyToolboxDiv[dir="RTL"][layout="h"] .blocklyToolboxCategoryContainer { +.blocklyToolbox[dir="RTL"][layout="h"] .blocklyToolboxCategoryContainer { margin: 1px 0 1px 5px; } @@ -679,7 +679,7 @@ Css.register(` white-space: nowrap; } -.blocklyToolboxDiv[dir="RTL"] .blocklyToolboxCategory { +.blocklyToolbox[dir="RTL"] .blocklyToolboxCategory { margin-left: 8px; padding-right: 0; } @@ -692,19 +692,19 @@ Css.register(` width: 16px; } -.blocklyTreeIconClosed { +.blocklyToolboxCategoryIconClosed { background-position: -32px -1px; } -.blocklyToolboxDiv[dir="RTL"] .blocklyTreeIconClosed { +.blocklyToolbox[dir="RTL"] .blocklyToolboxCategoryIconClosed { background-position: 0 -1px; } -.blocklyToolboxSelected>.blocklyTreeIconClosed { +.blocklyToolboxSelected>.blocklyToolboxCategoryIconClosed { background-position: -32px -17px; } -.blocklyToolboxDiv[dir="RTL"] .blocklyToolboxSelected>.blocklyTreeIconClosed { +.blocklyToolbox[dir="RTL"] .blocklyToolboxSelected>.blocklyToolboxCategoryIconClosed { background-position: 0 -17px; } @@ -716,18 +716,18 @@ Css.register(` background-position: -16px -17px; } -.blocklyTreeLabel { +.blocklyToolboxCategoryLabel { cursor: default; font: 16px sans-serif; padding: 0 3px; vertical-align: middle; } -.blocklyToolboxDelete .blocklyTreeLabel { +.blocklyToolboxDelete .blocklyToolboxCategoryLabel { cursor: url("<<>>/handdelete.cur"), auto; } -.blocklyToolboxSelected .blocklyTreeLabel { +.blocklyToolboxSelected .blocklyToolboxCategoryLabel { color: #fff; } `); diff --git a/core/toolbox/separator.ts b/core/toolbox/separator.ts index ec003daf686..5824b439316 100644 --- a/core/toolbox/separator.ts +++ b/core/toolbox/separator.ts @@ -88,7 +88,7 @@ Css.register(` margin: 5px 0; } -.blocklyToolboxDiv[layout="h"] .blocklyTreeSeparator { +.blocklyToolbox[layout="h"] .blocklyTreeSeparator { border-right: solid #e5e5e5 1px; border-bottom: none; height: auto; diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 0c5a8e2a4f2..efd5381b48b 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -198,7 +198,7 @@ export class Toolbox protected createContainer_(): HTMLDivElement { const toolboxContainer = document.createElement('div'); toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v'); - dom.addClass(toolboxContainer, 'blocklyToolboxDiv'); + dom.addClass(toolboxContainer, 'blocklyToolbox'); toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); return toolboxContainer; } @@ -1107,7 +1107,7 @@ Css.register(` } /* Category tree in Toolbox. */ -.blocklyToolboxDiv { +.blocklyToolbox { user-select: none; -ms-user-select: none; -webkit-user-select: none; diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js index 755f08cf8f2..1cb8df979ee 100644 --- a/tests/mocha/toolbox_test.js +++ b/tests/mocha/toolbox_test.js @@ -47,7 +47,7 @@ suite('Toolbox', function () { test('Init called -> HtmlDiv is inserted before parent node', function () { const toolboxDiv = Blockly.common.getMainWorkspace().getInjectionDiv() .childNodes[0]; - assert.equal(toolboxDiv.className, 'blocklyToolboxDiv'); + assert.equal(toolboxDiv.className, 'blocklyToolbox'); }); test('Init called -> Toolbox is subscribed to background and foreground colour', function () { const themeManager = this.toolbox.workspace_.getThemeManager(); From 8f2228658e9beaa67db6f4bd78638767ae47a2d3 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 7 Nov 2024 12:16:55 -0800 Subject: [PATCH 076/222] feat: Allow customizing GRID_UNIT for the Zelos renderer. (#8636) --- core/renderers/zelos/constants.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/core/renderers/zelos/constants.ts b/core/renderers/zelos/constants.ts index ddb2bdeef37..213d8a25967 100644 --- a/core/renderers/zelos/constants.ts +++ b/core/renderers/zelos/constants.ts @@ -151,9 +151,19 @@ export class ConstantProvider extends BaseConstantProvider { */ SQUARED: Shape | null = null; - constructor() { + /** + * Creates a new ConstantProvider. + * + * @param gridUnit If set, defines the base unit used to calculate other + * constants. + */ + constructor(gridUnit?: number) { super(); + if (gridUnit) { + this.GRID_UNIT = gridUnit; + } + this.SMALL_PADDING = this.GRID_UNIT; this.MEDIUM_PADDING = 2 * this.GRID_UNIT; From 7bbbb959f05e8d51b5c0b5dcc17bce48e21d8ca6 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 11 Nov 2024 07:54:17 -0800 Subject: [PATCH 077/222] feat!: Use CSS to specify field cursors. (#8648) --- core/css.ts | 13 +++++++++++++ core/field.ts | 5 ----- core/field_checkbox.ts | 5 ----- core/field_dropdown.ts | 8 +++++--- core/field_input.ts | 7 ++++--- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/core/css.ts b/core/css.ts index 22abfa32a22..0c547ca0a95 100644 --- a/core/css.ts +++ b/core/css.ts @@ -464,4 +464,17 @@ input[type=number] { z-index: 80; pointer-events: none; } + +.blocklyField { + cursor: default; +} + +.blocklyInputField { + cursor: text; +} + +.blocklyDragging .blocklyField, +.blocklyDragging .blocklyIconGroup { + cursor: grabbing; +} `; diff --git a/core/field.ts b/core/field.ts index 2d50c04eb5a..fbaf1cf6252 100644 --- a/core/field.ts +++ b/core/field.ts @@ -193,9 +193,6 @@ export abstract class Field */ SERIALIZABLE = false; - /** Mouse cursor style when over the hotspot that initiates the editor. */ - CURSOR = ''; - /** * @param value The initial value of the field. * Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by @@ -536,11 +533,9 @@ export abstract class Field if (this.enabled_ && block.isEditable()) { dom.addClass(group, 'blocklyEditableField'); dom.removeClass(group, 'blocklyNonEditableField'); - group.style.cursor = this.CURSOR; } else { dom.addClass(group, 'blocklyNonEditableField'); dom.removeClass(group, 'blocklyEditableField'); - group.style.cursor = ''; } } diff --git a/core/field_checkbox.ts b/core/field_checkbox.ts index 0773a1f8251..eb68be2e88d 100644 --- a/core/field_checkbox.ts +++ b/core/field_checkbox.ts @@ -35,11 +35,6 @@ export class FieldCheckbox extends Field { */ override SERIALIZABLE = true; - /** - * Mouse cursor style when over the hotspot that initiates editability. - */ - override CURSOR = 'default'; - /** * NOTE: The default value is set in `Field`, so maintain that value instead * of overwriting it here or in the constructor. diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 5f26ac3b403..a5f7830f6ee 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -70,9 +70,6 @@ export class FieldDropdown extends Field { */ override SERIALIZABLE = true; - /** Mouse cursor style when over the hotspot that initiates the editor. */ - override CURSOR = 'default'; - protected menuGenerator_?: MenuGenerator; /** A cache of the most recently generated options. */ @@ -204,6 +201,11 @@ export class FieldDropdown extends Field { if (this.borderRect_) { dom.addClass(this.borderRect_, 'blocklyDropdownRect'); } + + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyField'); + dom.addClass(this.fieldGroup_, 'blocklyDropdownField'); + } } /** diff --git a/core/field_input.ts b/core/field_input.ts index dcc2ac29ec4..5f845a6a2d5 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -99,9 +99,6 @@ export abstract class FieldInput extends Field< */ override SERIALIZABLE = true; - /** Mouse cursor style when over the hotspot that initiates the editor. */ - override CURSOR = 'text'; - /** * @param value The initial value of the field. Should cast to a string. * Defaults to an empty string if null or undefined. Also accepts @@ -148,6 +145,10 @@ export abstract class FieldInput extends Field< if (this.isFullBlockField()) { this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot(); } + + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyInputField'); + } } protected override isFullBlockField(): boolean { From ae2a14014144aca4ee22f8e70a065e8242cf7c36 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 12 Nov 2024 09:52:23 -0800 Subject: [PATCH 078/222] refactor!: Use one map for toolbox contents. (#8654) --- core/toolbox/toolbox.ts | 66 ++++++++----------- .../mocha/test_helpers/toolbox_definitions.js | 10 ++- tests/mocha/toolbox_test.js | 40 ++++++----- 3 files changed, 53 insertions(+), 63 deletions(-) diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index efd5381b48b..12465813c0d 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -70,9 +70,6 @@ export class Toolbox /** Whether the Toolbox is visible. */ protected isVisible_ = false; - /** The list of items in the toolbox. */ - protected contents_: IToolboxItem[] = []; - /** The width of the toolbox. */ protected width_ = 0; @@ -82,7 +79,10 @@ export class Toolbox /** The flyout for the toolbox. */ private flyout_: IFlyout | null = null; - protected contentMap_: {[key: string]: IToolboxItem}; + + /** Map from ID to the corresponding toolbox item. */ + protected contents = new Map(); + toolboxPosition: toolbox.Position; /** The currently selected item. */ @@ -118,9 +118,6 @@ export class Toolbox /** Is RTL vs LTR. */ this.RTL = workspace.options.RTL; - /** A map from toolbox item IDs to toolbox items. */ - this.contentMap_ = Object.create(null); - /** Position of the toolbox and flyout relative to the workspace. */ this.toolboxPosition = workspace.options.toolboxPosition; } @@ -367,14 +364,8 @@ export class Toolbox */ render(toolboxDef: toolbox.ToolboxInfo) { this.toolboxDef_ = toolboxDef; - for (let i = 0; i < this.contents_.length; i++) { - const toolboxItem = this.contents_[i]; - if (toolboxItem) { - toolboxItem.dispose(); - } - } - this.contents_ = []; - this.contentMap_ = Object.create(null); + this.contents.forEach((item) => item.dispose()); + this.contents.clear(); this.renderContents_(toolboxDef['contents']); this.position(); this.handleToolboxItemResize(); @@ -445,8 +436,7 @@ export class Toolbox * @param toolboxItem The item in the toolbox. */ protected addToolboxItem_(toolboxItem: IToolboxItem) { - this.contents_.push(toolboxItem); - this.contentMap_[toolboxItem.getId()] = toolboxItem; + this.contents.set(toolboxItem.getId(), toolboxItem); if (toolboxItem.isCollapsible()) { const collapsibleItem = toolboxItem as ICollapsibleToolboxItem; const childToolboxItems = collapsibleItem.getChildToolboxItems(); @@ -463,7 +453,7 @@ export class Toolbox * @returns The list of items in the toolbox. */ getToolboxItems(): IToolboxItem[] { - return this.contents_; + return [...this.contents.values()]; } /** @@ -618,7 +608,7 @@ export class Toolbox * @returns The toolbox item with the given ID, or null if no item exists. */ getToolboxItemById(id: string): IToolboxItem | null { - return this.contentMap_[id] || null; + return this.contents.get(id) || null; } /** @@ -765,14 +755,13 @@ export class Toolbox * @internal */ refreshTheme() { - for (let i = 0; i < this.contents_.length; i++) { - const child = this.contents_[i]; + this.contents.forEach((child) => { // TODO(#6097): Fix types or add refreshTheme to IToolboxItem. const childAsCategory = child as ToolboxCategory; if (childAsCategory.refreshTheme) { childAsCategory.refreshTheme(); } - } + }); } /** @@ -923,11 +912,9 @@ export class Toolbox * @param position The position of the item to select. */ selectItemByPosition(position: number) { - if (position > -1 && position < this.contents_.length) { - const item = this.contents_[position]; - if (item.isSelectable()) { - this.setSelectedItem(item); - } + const item = this.getToolboxItems()[position]; + if (item) { + this.setSelectedItem(item); } } @@ -1034,11 +1021,12 @@ export class Toolbox return false; } - let nextItemIdx = this.contents_.indexOf(this.selectedItem_) + 1; - if (nextItemIdx > -1 && nextItemIdx < this.contents_.length) { - let nextItem = this.contents_[nextItemIdx]; + const items = [...this.contents.values()]; + let nextItemIdx = items.indexOf(this.selectedItem_) + 1; + if (nextItemIdx > -1 && nextItemIdx < items.length) { + let nextItem = items[nextItemIdx]; while (nextItem && !nextItem.isSelectable()) { - nextItem = this.contents_[++nextItemIdx]; + nextItem = items[++nextItemIdx]; } if (nextItem && nextItem.isSelectable()) { this.setSelectedItem(nextItem); @@ -1058,11 +1046,12 @@ export class Toolbox return false; } - let prevItemIdx = this.contents_.indexOf(this.selectedItem_) - 1; - if (prevItemIdx > -1 && prevItemIdx < this.contents_.length) { - let prevItem = this.contents_[prevItemIdx]; + const items = [...this.contents.values()]; + let prevItemIdx = items.indexOf(this.selectedItem_) - 1; + if (prevItemIdx > -1 && prevItemIdx < items.length) { + let prevItem = items[prevItemIdx]; while (prevItem && !prevItem.isSelectable()) { - prevItem = this.contents_[--prevItemIdx]; + prevItem = items[--prevItemIdx]; } if (prevItem && prevItem.isSelectable()) { this.setSelectedItem(prevItem); @@ -1076,16 +1065,13 @@ export class Toolbox dispose() { this.workspace_.getComponentManager().removeComponent('toolbox'); this.flyout_!.dispose(); - for (let i = 0; i < this.contents_.length; i++) { - const toolboxItem = this.contents_[i]; - toolboxItem.dispose(); - } + this.contents.forEach((item) => item.dispose()); for (let j = 0; j < this.boundEvents_.length; j++) { browserEvents.unbind(this.boundEvents_[j]); } this.boundEvents_ = []; - this.contents_ = []; + this.contents.clear(); if (this.HtmlDiv) { this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv); diff --git a/tests/mocha/test_helpers/toolbox_definitions.js b/tests/mocha/test_helpers/toolbox_definitions.js index 2f767ed60b7..f05c2962075 100644 --- a/tests/mocha/test_helpers/toolbox_definitions.js +++ b/tests/mocha/test_helpers/toolbox_definitions.js @@ -243,9 +243,8 @@ export function getBasicToolbox() { } export function getCollapsibleItem(toolbox) { - const contents = toolbox.contents_; - for (let i = 0; i < contents.length; i++) { - const item = contents[i]; + const contents = toolbox.contents.values(); + for (const item of contents) { if (item.isCollapsible()) { return item; } @@ -253,9 +252,8 @@ export function getCollapsibleItem(toolbox) { } export function getNonCollapsibleItem(toolbox) { - const contents = toolbox.contents_; - for (let i = 0; i < contents.length; i++) { - const item = contents[i]; + const contents = toolbox.contents.values(); + for (const item of contents) { if (!item.isCollapsible()) { return item; } diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js index 1cb8df979ee..c6a1c726dc4 100644 --- a/tests/mocha/toolbox_test.js +++ b/tests/mocha/toolbox_test.js @@ -98,7 +98,7 @@ suite('Toolbox', function () { {'kind': 'category', 'contents': []}, ], }); - assert.lengthOf(this.toolbox.contents_, 2); + assert.equal(this.toolbox.contents.size, 2); sinon.assert.called(positionStub); }); // TODO: Uncomment once implemented. @@ -153,7 +153,7 @@ suite('Toolbox', function () { ], }; this.toolbox.render(jsonDef); - assert.lengthOf(this.toolbox.contents_, 1); + assert.equal(this.toolbox.contents.size, 1); }); test('multiple icon classes can be applied', function () { const jsonDef = { @@ -176,7 +176,7 @@ suite('Toolbox', function () { assert.doesNotThrow(() => { this.toolbox.render(jsonDef); }); - assert.lengthOf(this.toolbox.contents_, 1); + assert.equal(this.toolbox.contents.size, 1); }); }); @@ -204,7 +204,7 @@ suite('Toolbox', function () { const evt = { 'target': categoryXml, }; - const item = this.toolbox.contentMap_[categoryXml.getAttribute('id')]; + const item = this.toolbox.contents.get(categoryXml.getAttribute('id')); const setSelectedSpy = sinon.spy(this.toolbox, 'setSelectedItem'); const onClickSpy = sinon.spy(item, 'onClick'); this.toolbox.onClick_(evt); @@ -356,14 +356,16 @@ suite('Toolbox', function () { assert.isFalse(handled); }); test('Next item is selectable -> Should select next item', function () { - const item = this.toolbox.contents_[0]; + const items = [...this.toolbox.contents.values()]; + const item = items[0]; this.toolbox.selectedItem_ = item; const handled = this.toolbox.selectNext_(); assert.isTrue(handled); - assert.equal(this.toolbox.selectedItem_, this.toolbox.contents_[1]); + assert.equal(this.toolbox.selectedItem_, items[1]); }); test('Selected item is last item -> Should not handle event', function () { - const item = this.toolbox.contents_[this.toolbox.contents_.length - 1]; + const items = [...this.toolbox.contents.values()]; + const item = items.at(-1); this.toolbox.selectedItem_ = item; const handled = this.toolbox.selectNext_(); assert.isFalse(handled); @@ -387,15 +389,16 @@ suite('Toolbox', function () { assert.isFalse(handled); }); test('Selected item is first item -> Should not handle event', function () { - const item = this.toolbox.contents_[0]; + const item = [...this.toolbox.contents.values()][0]; this.toolbox.selectedItem_ = item; const handled = this.toolbox.selectPrevious_(); assert.isFalse(handled); assert.equal(this.toolbox.selectedItem_, item); }); test('Previous item is selectable -> Should select previous item', function () { - const item = this.toolbox.contents_[1]; - const prevItem = this.toolbox.contents_[0]; + const items = [...this.toolbox.contents.values()]; + const item = items[1]; + const prevItem = items[0]; this.toolbox.selectedItem_ = item; const handled = this.toolbox.selectPrevious_(); assert.isTrue(handled); @@ -404,9 +407,10 @@ suite('Toolbox', function () { test('Previous item is collapsed -> Should skip over children of the previous item', function () { const childItem = getChildItem(this.toolbox); const parentItem = childItem.getParent(); - const parentIdx = this.toolbox.contents_.indexOf(parentItem); + const items = [...this.toolbox.contents.values()]; + const parentIdx = items.indexOf(parentItem); // Gets the item after the parent. - const item = this.toolbox.contents_[parentIdx + 1]; + const item = items[parentIdx + 1]; this.toolbox.selectedItem_ = item; const handled = this.toolbox.selectPrevious_(); assert.isTrue(handled); @@ -728,9 +732,10 @@ suite('Toolbox', function () { }); test('Child categories visible if all ancestors expanded', function () { this.toolbox.render(getDeeplyNestedJSON()); - const outerCategory = this.toolbox.contents_[0]; - const middleCategory = this.toolbox.contents_[1]; - const innerCategory = this.toolbox.contents_[2]; + const items = [...this.toolbox.contents.values()]; + const outerCategory = items[0]; + const middleCategory = items[1]; + const innerCategory = items[2]; outerCategory.toggleExpanded(); middleCategory.toggleExpanded(); @@ -743,8 +748,9 @@ suite('Toolbox', function () { }); test('Child categories not visible if any ancestor not expanded', function () { this.toolbox.render(getDeeplyNestedJSON()); - const middleCategory = this.toolbox.contents_[1]; - const innerCategory = this.toolbox.contents_[2]; + const items = [...this.toolbox.contents.values()]; + const middleCategory = items[1]; + const innerCategory = items[2]; // Don't expand the outermost category // Even though the direct parent of inner is expanded, it shouldn't be visible From af5905a3e6571d0e9a6973cc4104ba7972299ff8 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 12 Nov 2024 11:45:20 -0800 Subject: [PATCH 079/222] refactor!: Add setSelectedItem() to IToolbox. (#8650) --- core/interfaces/i_toolbox.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/interfaces/i_toolbox.ts b/core/interfaces/i_toolbox.ts index a236d4442ae..e45bc6c0467 100644 --- a/core/interfaces/i_toolbox.ts +++ b/core/interfaces/i_toolbox.ts @@ -94,7 +94,7 @@ export interface IToolbox extends IRegistrable { setVisible(isVisible: boolean): void; /** - * Selects the toolbox item by it's position in the list of toolbox items. + * Selects the toolbox item by its position in the list of toolbox items. * * @param position The position of the item to select. */ @@ -107,6 +107,14 @@ export interface IToolbox extends IRegistrable { */ getSelectedItem(): IToolboxItem | null; + /** + * Sets the selected item. + * + * @param item The toolbox item to select, or null to remove the current + * selection. + */ + setSelectedItem(item: IToolboxItem | null): void; + /** Disposes of this toolbox. */ dispose(): void; } From 4230956244aa0c61c7d1feedcebeea5294dedaba Mon Sep 17 00:00:00 2001 From: John Nesky Date: Mon, 2 Dec 2024 13:34:05 -0800 Subject: [PATCH 080/222] fix: Create CSS vars for SVG patterns. (#8671) --- core/bubbles/bubble.ts | 6 +--- core/css.ts | 9 ++++++ core/grid.ts | 15 ++++++++++ core/inject.ts | 11 ++++++-- core/renderers/common/constants.ts | 30 +++++++++++++++++++- core/renderers/common/path_object.ts | 11 -------- core/renderers/common/renderer.ts | 27 +++++++++++++++--- core/renderers/geras/renderer.ts | 8 ++++-- core/renderers/zelos/constants.ts | 34 +++++++++++++++++++++-- core/renderers/zelos/path_object.ts | 14 +--------- core/workspace_svg.ts | 41 ++++++++++++++++++---------- 11 files changed, 150 insertions(+), 56 deletions(-) diff --git a/core/bubbles/bubble.ts b/core/bubbles/bubble.ts index 35b9e7dde0a..cac1c648528 100644 --- a/core/bubbles/bubble.ts +++ b/core/bubbles/bubble.ts @@ -106,11 +106,7 @@ export abstract class Bubble implements IBubble, ISelectable { ); const embossGroup = dom.createSvgElement( Svg.G, - { - 'filter': `url(#${ - this.workspace.getRenderer().getConstants().embossFilterId - })`, - }, + {'class': 'blocklyEmboss'}, this.svgRoot, ); this.tail = dom.createSvgElement( diff --git a/core/css.ts b/core/css.ts index 0c547ca0a95..fe507141167 100644 --- a/core/css.ts +++ b/core/css.ts @@ -85,6 +85,10 @@ let content = ` transition: transform .5s; } +.blocklyEmboss { + filter: var(--blocklyEmbossFilter); +} + .blocklyTooltipDiv { background-color: #ffffc7; border: 1px solid #ddc; @@ -138,6 +142,10 @@ let content = ` border-color: inherit; } +.blocklyHighlighted>.blocklyPath { + filter: var(--blocklyEmbossFilter); +} + .blocklyHighlightedConnectionPath { fill: none; stroke: #fc3; @@ -189,6 +197,7 @@ let content = ` } .blocklyDisabled>.blocklyPath { + fill: var(--blocklyDisabledPattern); fill-opacity: .5; stroke-opacity: .5; } diff --git a/core/grid.ts b/core/grid.ts index 1a5de250e5c..45c1674f9b7 100644 --- a/core/grid.ts +++ b/core/grid.ts @@ -210,6 +210,9 @@ export class Grid { * @param rnd A random ID to append to the pattern's ID. * @param gridOptions The object containing grid configuration. * @param defs The root SVG element for this workspace's defs. + * @param injectionDiv The div containing the parent workspace and all related + * workspaces and block containers. CSS variables representing SVG patterns + * will be scoped to this container. * @returns The SVG element for the grid pattern. * @internal */ @@ -217,6 +220,7 @@ export class Grid { rnd: string, gridOptions: GridOptions, defs: SVGElement, + injectionDiv?: HTMLElement, ): SVGElement { /* @@ -247,6 +251,17 @@ export class Grid { // Edge 16 doesn't handle empty patterns dom.createSvgElement(Svg.LINE, {}, gridPattern); } + + if (injectionDiv) { + // Add CSS variables scoped to the injection div referencing the created + // patterns so that CSS can apply the patterns to any element in the + // injection div. + injectionDiv.style.setProperty( + '--blocklyGridPattern', + `url(#${gridPattern.id})`, + ); + } + return gridPattern; } } diff --git a/core/inject.ts b/core/inject.ts index 55409b7f3d2..d04b23ed766 100644 --- a/core/inject.ts +++ b/core/inject.ts @@ -89,7 +89,7 @@ export function inject( * @param options Dictionary of options. * @returns Newly created SVG image. */ -function createDom(container: Element, options: Options): SVGElement { +function createDom(container: HTMLElement, options: Options): SVGElement { // Sadly browsers (Chrome vs Firefox) are currently inconsistent in laying // out content in RTL mode. Therefore Blockly forces the use of LTR, // then manually positions content in RTL as needed. @@ -132,7 +132,12 @@ function createDom(container: Element, options: Options): SVGElement { // https://neil.fraser.name/news/2015/11/01/ const rnd = String(Math.random()).substring(2); - options.gridPattern = Grid.createDom(rnd, options.gridOptions, defs); + options.gridPattern = Grid.createDom( + rnd, + options.gridOptions, + defs, + container, + ); return svg; } @@ -144,7 +149,7 @@ function createDom(container: Element, options: Options): SVGElement { * @returns Newly created main workspace. */ function createMainWorkspace( - injectionDiv: Element, + injectionDiv: HTMLElement, svg: SVGElement, options: Options, ): WorkspaceSvg { diff --git a/core/renderers/common/constants.ts b/core/renderers/common/constants.ts index 01217edb725..c5a7a759c5c 100644 --- a/core/renderers/common/constants.ts +++ b/core/renderers/common/constants.ts @@ -926,8 +926,18 @@ export class ConstantProvider { * @param svg The root of the workspace's SVG. * @param tagName The name to use for the CSS style tag. * @param selector The CSS selector to use. + * @param injectionDivIfIsParent The div containing the parent workspace and + * all related workspaces and block containers, if this renderer is for the + * parent workspace. CSS variables representing SVG patterns will be scoped + * to this container. Child workspaces should not override the CSS variables + * created by the parent and thus do not need access to the injection div. */ - createDom(svg: SVGElement, tagName: string, selector: string) { + createDom( + svg: SVGElement, + tagName: string, + selector: string, + injectionDivIfIsParent?: HTMLElement, + ) { this.injectCSS_(tagName, selector); /* @@ -1034,6 +1044,24 @@ export class ConstantProvider { this.disabledPattern = disabledPattern; this.createDebugFilter(); + + if (injectionDivIfIsParent) { + // If this renderer is for the parent workspace, add CSS variables scoped + // to the injection div referencing the created patterns so that CSS can + // apply the patterns to any element in the injection div. + injectionDivIfIsParent.style.setProperty( + '--blocklyEmbossFilter', + `url(#${this.embossFilterId})`, + ); + injectionDivIfIsParent.style.setProperty( + '--blocklyDisabledPattern', + `url(#${this.disabledPatternId})`, + ); + injectionDivIfIsParent.style.setProperty( + '--blocklyDebugFilter', + `url(#${this.debugFilterId})`, + ); + } } /** diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index 12e23b6c4aa..823ab678525 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -173,13 +173,8 @@ export class PathObject implements IPathObject { updateHighlighted(enable: boolean) { if (enable) { - this.svgPath.setAttribute( - 'filter', - 'url(#' + this.constants.embossFilterId + ')', - ); this.setClass_('blocklyHighlighted', true); } else { - this.svgPath.setAttribute('filter', 'none'); this.setClass_('blocklyHighlighted', false); } } @@ -206,12 +201,6 @@ export class PathObject implements IPathObject { */ protected updateDisabled_(disabled: boolean) { this.setClass_('blocklyDisabled', disabled); - if (disabled) { - this.svgPath.setAttribute( - 'fill', - 'url(#' + this.constants.disabledPatternId + ')', - ); - } } /** diff --git a/core/renderers/common/renderer.ts b/core/renderers/common/renderer.ts index 255bc81ff52..62d392a8bd5 100644 --- a/core/renderers/common/renderer.ts +++ b/core/renderers/common/renderer.ts @@ -78,13 +78,23 @@ export class Renderer implements IRegistrable { * * @param svg The root of the workspace's SVG. * @param theme The workspace theme object. + * @param injectionDivIfIsParent The div containing the parent workspace and + * all related workspaces and block containers, if this renderer is for the + * parent workspace. CSS variables representing SVG patterns will be scoped + * to this container. Child workspaces should not override the CSS variables + * created by the parent and thus do not need access to the injection div. * @internal */ - createDom(svg: SVGElement, theme: Theme) { + createDom( + svg: SVGElement, + theme: Theme, + injectionDivIfIsParent?: HTMLElement, + ) { this.constants_.createDom( svg, this.name + '-' + theme.name, '.' + this.getClassName() + '.' + theme.getClassName(), + injectionDivIfIsParent, ); } @@ -93,8 +103,17 @@ export class Renderer implements IRegistrable { * * @param svg The root of the workspace's SVG. * @param theme The workspace theme object. - */ - refreshDom(svg: SVGElement, theme: Theme) { + * @param injectionDivIfIsParent The div containing the parent workspace and + * all related workspaces and block containers, if this renderer is for the + * parent workspace. CSS variables representing SVG patterns will be scoped + * to this container. Child workspaces should not override the CSS variables + * created by the parent and thus do not need access to the injection div. + */ + refreshDom( + svg: SVGElement, + theme: Theme, + injectionDivIfIsParent?: HTMLElement, + ) { const previousConstants = this.getConstants(); previousConstants.dispose(); this.constants_ = this.makeConstants_(); @@ -105,7 +124,7 @@ export class Renderer implements IRegistrable { this.constants_.randomIdentifier = previousConstants.randomIdentifier; this.constants_.setTheme(theme); this.constants_.init(); - this.createDom(svg, theme); + this.createDom(svg, theme, injectionDivIfIsParent); } /** diff --git a/core/renderers/geras/renderer.ts b/core/renderers/geras/renderer.ts index 06062e9bc30..635391c8128 100644 --- a/core/renderers/geras/renderer.ts +++ b/core/renderers/geras/renderer.ts @@ -50,8 +50,12 @@ export class Renderer extends BaseRenderer { this.highlightConstants.init(); } - override refreshDom(svg: SVGElement, theme: Theme) { - super.refreshDom(svg, theme); + override refreshDom( + svg: SVGElement, + theme: Theme, + injectionDiv: HTMLElement, + ) { + super.refreshDom(svg, theme, injectionDiv); this.getHighlightConstants().init(); } diff --git a/core/renderers/zelos/constants.ts b/core/renderers/zelos/constants.ts index 213d8a25967..3677d347792 100644 --- a/core/renderers/zelos/constants.ts +++ b/core/renderers/zelos/constants.ts @@ -675,8 +675,13 @@ export class ConstantProvider extends BaseConstantProvider { return utilsColour.blend('#000', colour, 0.25) || colour; } - override createDom(svg: SVGElement, tagName: string, selector: string) { - super.createDom(svg, tagName, selector); + override createDom( + svg: SVGElement, + tagName: string, + selector: string, + injectionDivIfIsParent?: HTMLElement, + ) { + super.createDom(svg, tagName, selector, injectionDivIfIsParent); /* ... filters go here ... @@ -795,6 +800,20 @@ export class ConstantProvider extends BaseConstantProvider { ); this.replacementGlowFilterId = replacementGlowFilter.id; this.replacementGlowFilter = replacementGlowFilter; + + if (injectionDivIfIsParent) { + // If this renderer is for the parent workspace, add CSS variables scoped + // to the injection div referencing the created patterns so that CSS can + // apply the patterns to any element in the injection div. + injectionDivIfIsParent.style.setProperty( + '--blocklySelectedGlowFilter', + `url(#${this.selectedGlowFilterId})`, + ); + injectionDivIfIsParent.style.setProperty( + '--blocklyReplacementGlowFilter', + `url(#${this.replacementGlowFilterId})`, + ); + } } override getCSS_(selector: string) { @@ -873,7 +892,7 @@ export class ConstantProvider extends BaseConstantProvider { // Disabled outline paths. `${selector} .blocklyDisabled > .blocklyOutlinePath {`, - `fill: url(#blocklyDisabledPattern${this.randomIdentifier})`, + `fill: var(--blocklyDisabledPattern)`, `}`, // Insertion marker. @@ -881,6 +900,15 @@ export class ConstantProvider extends BaseConstantProvider { `fill-opacity: ${this.INSERTION_MARKER_OPACITY};`, `stroke: none;`, `}`, + + `${selector} .blocklySelected>.blocklyPath.blocklyPathSelected {`, + `fill: none;`, + `filter: var(--blocklySelectedGlowFilter);`, + `}`, + + `${selector} .blocklyReplaceable>.blocklyPath {`, + `filter: var(--blocklyReplacementGlowFilter);`, + `}`, ]; } } diff --git a/core/renderers/zelos/path_object.ts b/core/renderers/zelos/path_object.ts index fdc6ab8a626..6cb3a0506ef 100644 --- a/core/renderers/zelos/path_object.ts +++ b/core/renderers/zelos/path_object.ts @@ -91,11 +91,7 @@ export class PathObject extends BasePathObject { if (enable) { if (!this.svgPathSelected) { this.svgPathSelected = this.svgPath.cloneNode(true) as SVGElement; - this.svgPathSelected.setAttribute('fill', 'none'); - this.svgPathSelected.setAttribute( - 'filter', - 'url(#' + this.constants.selectedGlowFilterId + ')', - ); + this.svgPathSelected.classList.add('blocklyPathSelected'); this.svgRoot.appendChild(this.svgPathSelected); } } else { @@ -108,14 +104,6 @@ export class PathObject extends BasePathObject { override updateReplacementFade(enable: boolean) { this.setClass_('blocklyReplaceable', enable); - if (enable) { - this.svgPath.setAttribute( - 'filter', - 'url(#' + this.constants.replacementGlowFilterId + ')', - ); - } else { - this.svgPath.removeAttribute('filter'); - } } override updateShapeForInputHighlight(conn: Connection, enable: boolean) { diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 5447bdf51b8..30dcaaeb9a9 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -225,7 +225,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * The first parent div with 'injectionDiv' in the name, or null if not set. * Access this with getInjectionDiv. */ - private injectionDiv: Element | null = null; + private injectionDiv: HTMLElement | null = null; /** * Last known position of the page scroll. @@ -539,7 +539,12 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { */ refreshTheme() { if (this.svgGroup_) { - this.renderer.refreshDom(this.svgGroup_, this.getTheme()); + const isParentWorkspace = this.options.parentWorkspace === null; + this.renderer.refreshDom( + this.svgGroup_, + this.getTheme(), + isParentWorkspace ? this.getInjectionDiv() : undefined, + ); } // Update all blocks in workspace that have a style name. @@ -636,20 +641,24 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { // Before the SVG canvas, scale the coordinates. scale = this.scale; } + let ancestor: Element = element; do { // Loop through this block and every parent. - const xy = svgMath.getRelativeXY(element); - if (element === this.getCanvas() || element === this.getBubbleCanvas()) { + const xy = svgMath.getRelativeXY(ancestor); + if ( + ancestor === this.getCanvas() || + ancestor === this.getBubbleCanvas() + ) { // After the SVG canvas, don't scale the coordinates. scale = 1; } x += xy.x * scale; y += xy.y * scale; - element = element.parentNode as SVGElement; + ancestor = ancestor.parentNode as Element; } while ( - element && - element !== this.getParentSvg() && - element !== this.getInjectionDiv() + ancestor && + ancestor !== this.getParentSvg() && + ancestor !== this.getInjectionDiv() ); return new Coordinate(x, y); } @@ -687,7 +696,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @returns The first parent div with 'injectionDiv' in the name. * @internal */ - getInjectionDiv(): Element { + getInjectionDiv(): HTMLElement { // NB: it would be better to pass this in at createDom, but is more likely // to break existing uses of Blockly. if (!this.injectionDiv) { @@ -695,7 +704,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { while (element) { const classes = element.getAttribute('class') || ''; if ((' ' + classes + ' ').includes(' injectionDiv ')) { - this.injectionDiv = element; + this.injectionDiv = element as HTMLElement; break; } element = element.parentNode as Element; @@ -739,7 +748,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * 'blocklyMutatorBackground'. * @returns The workspace's SVG group. */ - createDom(opt_backgroundClass?: string, injectionDiv?: Element): Element { + createDom(opt_backgroundClass?: string, injectionDiv?: HTMLElement): Element { if (!this.injectionDiv) { this.injectionDiv = injectionDiv ?? null; } @@ -765,8 +774,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { ); if (opt_backgroundClass === 'blocklyMainBackground' && this.grid) { - this.svgBackground_.style.fill = - 'url(#' + this.grid.getPatternId() + ')'; + this.svgBackground_.style.fill = 'var(--blocklyGridPattern)'; } else { this.themeManager_.subscribe( this.svgBackground_, @@ -823,7 +831,12 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { CursorClass && this.markerManager.setCursor(new CursorClass()); - this.renderer.createDom(this.svgGroup_, this.getTheme()); + const isParentWorkspace = this.options.parentWorkspace === null; + this.renderer.createDom( + this.svgGroup_, + this.getTheme(), + isParentWorkspace ? this.getInjectionDiv() : undefined, + ); return this.svgGroup_; } From 389dd1a1cb8fc8c16c5ce0fdc1b1df0bf84b62fb Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 4 Dec 2024 12:15:19 -0800 Subject: [PATCH 081/222] chore: Post-merge fixits. --- core/block.ts | 8 +++--- core/block_flyout_inflater.ts | 28 +++++++++---------- core/blockly.ts | 8 ------ core/button_flyout_inflater.ts | 12 ++++---- core/events/events_var_type_change.ts | 10 +++---- core/events/type.ts | 2 ++ core/field_textinput.ts | 2 +- core/field_variable.ts | 6 ++-- core/flyout_base.ts | 4 +-- core/icons/comment_icon.ts | 1 - core/interfaces/i_comment_icon.ts | 2 +- core/interfaces/i_flyout_inflater.ts | 6 ++-- .../i_variable_backed_parameter_model.ts | 2 +- core/label_flyout_inflater.ts | 12 ++++---- core/names.ts | 2 +- core/renderers/common/renderer.ts | 1 - core/renderers/zelos/renderer.ts | 1 - core/separator_flyout_inflater.ts | 12 ++++---- core/serialization/blocks.ts | 1 - core/serialization/variables.ts | 2 +- core/variable_model.ts | 3 +- core/variables.ts | 2 +- core/xml.ts | 8 +++--- 23 files changed, 63 insertions(+), 72 deletions(-) diff --git a/core/block.ts b/core/block.ts index c3dc037c5c3..9c68bc4cded 100644 --- a/core/block.ts +++ b/core/block.ts @@ -43,6 +43,10 @@ import {ValueInput} from './inputs/value_input.js'; import type {IASTNodeLocation} from './interfaces/i_ast_node_location.js'; import {isCommentIcon} from './interfaces/i_comment_icon.js'; import {type IIcon} from './interfaces/i_icon.js'; +import type { + IVariableModel, + IVariableState, +} from './interfaces/i_variable_model.js'; import * as registry from './registry.js'; import * as Tooltip from './tooltip.js'; import * as arrayUtils from './utils/array.js'; @@ -51,10 +55,6 @@ import * as deprecation from './utils/deprecation.js'; import * as idGenerator from './utils/idgenerator.js'; import * as parsing from './utils/parsing.js'; import {Size} from './utils/size.js'; -import type { - IVariableModel, - IVariableState, -} from './interfaces/i_variable_model.js'; import type {Workspace} from './workspace.js'; /** diff --git a/core/block_flyout_inflater.ts b/core/block_flyout_inflater.ts index b22d2a82171..b888e5f3a81 100644 --- a/core/block_flyout_inflater.ts +++ b/core/block_flyout_inflater.ts @@ -4,21 +4,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {IFlyout} from './interfaces/i_flyout.js'; -import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; -import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import {BlockSvg} from './block_svg.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; -import * as utilsXml from './utils/xml.js'; -import * as eventUtils from './events/utils.js'; -import * as Xml from './xml.js'; -import * as blocks from './serialization/blocks.js'; +import * as browserEvents from './browser_events.js'; import * as common from './common.js'; -import * as registry from './registry.js'; import {MANUALLY_DISABLED} from './constants.js'; import type {Abstract as AbstractEvent} from './events/events_abstract.js'; +import {EventType} from './events/type.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {IFlyout} from './interfaces/i_flyout.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import * as registry from './registry.js'; +import * as blocks from './serialization/blocks.js'; import type {BlockInfo} from './utils/toolbox.js'; -import * as browserEvents from './browser_events.js'; +import * as utilsXml from './utils/xml.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; +import * as Xml from './xml.js'; /** * The language-neutral ID for when the reason why a block is disabled is @@ -51,7 +51,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater { * @param flyoutWorkspace The workspace to create the block on. * @returns A newly created block. */ - load(state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { + load(state: object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { this.setFlyoutWorkspace(flyoutWorkspace); this.flyout = flyoutWorkspace.targetWorkspace?.getFlyout() ?? undefined; const block = this.createBlock(state as BlockInfo, flyoutWorkspace); @@ -114,7 +114,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater { * @param defaultGap The default spacing for flyout items. * @returns The amount of space that should follow this block. */ - gapForElement(state: Object, defaultGap: number): number { + gapForElement(state: object, defaultGap: number): number { const blockState = state as BlockInfo; let gap; if (blockState['gap']) { @@ -245,8 +245,8 @@ export class BlockFlyoutInflater implements IFlyoutInflater { !this.flyoutWorkspace || (event && !( - event.type === eventUtils.BLOCK_CREATE || - event.type === eventUtils.BLOCK_DELETE + event.type === EventType.BLOCK_CREATE || + event.type === EventType.BLOCK_DELETE )) ) return; diff --git a/core/blockly.ts b/core/blockly.ts index 5d0a9ae15b7..d0543abbe15 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -483,7 +483,6 @@ export { BlockFlyoutInflater, ButtonFlyoutInflater, CodeGenerator, - CodeGenerator, Field, FieldCheckbox, FieldCheckboxConfig, @@ -495,18 +494,11 @@ export { FieldDropdownFromJsonConfig, FieldDropdownValidator, FieldImage, - FieldImage, FieldImageConfig, - FieldImageConfig, - FieldImageFromJsonConfig, FieldImageFromJsonConfig, FieldLabel, - FieldLabel, - FieldLabelConfig, FieldLabelConfig, FieldLabelFromJsonConfig, - FieldLabelFromJsonConfig, - FieldLabelSerializable, FieldLabelSerializable, FieldNumber, FieldNumberConfig, diff --git a/core/button_flyout_inflater.ts b/core/button_flyout_inflater.ts index 703dc606938..fc788ea5b90 100644 --- a/core/button_flyout_inflater.ts +++ b/core/button_flyout_inflater.ts @@ -4,12 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; -import type {IBoundedElement} from './interfaces/i_bounded_element.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; import {FlyoutButton} from './flyout_button.js'; -import {ButtonOrLabelInfo} from './utils/toolbox.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import * as registry from './registry.js'; +import {ButtonOrLabelInfo} from './utils/toolbox.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; /** * Class responsible for creating buttons for flyouts. @@ -22,7 +22,7 @@ export class ButtonFlyoutInflater implements IFlyoutInflater { * @param flyoutWorkspace The workspace to create the button on. * @returns A newly created FlyoutButton. */ - load(state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { + load(state: object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { const button = new FlyoutButton( flyoutWorkspace, flyoutWorkspace.targetWorkspace!, @@ -40,7 +40,7 @@ export class ButtonFlyoutInflater implements IFlyoutInflater { * @param defaultGap The default spacing for flyout items. * @returns The amount of space that should follow this button. */ - gapForElement(state: Object, defaultGap: number): number { + gapForElement(state: object, defaultGap: number): number { return defaultGap; } diff --git a/core/events/events_var_type_change.ts b/core/events/events_var_type_change.ts index ab86866203c..c02a7e45435 100644 --- a/core/events/events_var_type_change.ts +++ b/core/events/events_var_type_change.ts @@ -10,21 +10,21 @@ * @class */ -import * as registry from '../registry.js'; import type { IVariableModel, IVariableState, } from '../interfaces/i_variable_model.js'; +import * as registry from '../registry.js'; -import {VarBase, VarBaseJson} from './events_var_base.js'; -import * as eventUtils from './utils.js'; import type {Workspace} from '../workspace.js'; +import {VarBase, VarBaseJson} from './events_var_base.js'; +import {EventType} from './type.js'; /** * Notifies listeners that a variable's type has changed. */ export class VarTypeChange extends VarBase { - override type = eventUtils.VAR_TYPE_CHANGE; + override type = EventType.VAR_TYPE_CHANGE; /** * @param variable The variable whose type changed. Undefined for a blank event. @@ -117,6 +117,6 @@ export interface VarTypeChangeJson extends VarBaseJson { registry.register( registry.Type.EVENT, - eventUtils.VAR_TYPE_CHANGE, + EventType.VAR_TYPE_CHANGE, VarTypeChange, ); diff --git a/core/events/type.ts b/core/events/type.ts index db9ad6c96a3..0928b8ff077 100644 --- a/core/events/type.ts +++ b/core/events/type.ts @@ -28,6 +28,8 @@ export enum EventType { VAR_DELETE = 'var_delete', /** Type of event that renames a variable. */ VAR_RENAME = 'var_rename', + /** Type of event that changes the type of a variable. */ + VAR_TYPE_CHANGE = 'var_type_change', /** * Type of generic event that records a UI change. * diff --git a/core/field_textinput.ts b/core/field_textinput.ts index 5b754624aff..2b896ad47be 100644 --- a/core/field_textinput.ts +++ b/core/field_textinput.ts @@ -21,8 +21,8 @@ import { FieldInputValidator, } from './field_input.js'; import * as fieldRegistry from './field_registry.js'; -import * as parsing from './utils/parsing.js'; import * as dom from './utils/dom.js'; +import * as parsing from './utils/parsing.js'; /** * Class for an editable text field. diff --git a/core/field_variable.ts b/core/field_variable.ts index 0c890f4d7bb..ad9037e9671 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -22,17 +22,17 @@ import { MenuGenerator, MenuOption, } from './field_dropdown.js'; -import * as dom from './utils/dom.js'; import * as fieldRegistry from './field_registry.js'; +import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import * as internalConstants from './internal_constants.js'; import type {Menu} from './menu.js'; import type {MenuItem} from './menuitem.js'; -import {WorkspaceSvg} from './workspace_svg.js'; import {Msg} from './msg.js'; +import * as dom from './utils/dom.js'; import * as parsing from './utils/parsing.js'; import {Size} from './utils/size.js'; -import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import * as Variables from './variables.js'; +import {WorkspaceSvg} from './workspace_svg.js'; import * as Xml from './xml.js'; /** diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 5b41a8004fc..54248edfd12 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -642,8 +642,8 @@ export abstract class Flyout // user typing long strings into fields on the blocks in the flyout. this.reflowWrapper = (event) => { if ( - event.type === eventUtils.BLOCK_CHANGE || - event.type === eventUtils.BLOCK_FIELD_INTERMEDIATE_CHANGE + event.type === EventType.BLOCK_CHANGE || + event.type === EventType.BLOCK_FIELD_INTERMEDIATE_CHANGE ) { this.reflow(); } diff --git a/core/icons/comment_icon.ts b/core/icons/comment_icon.ts index f252c4f59bc..78bf601f708 100644 --- a/core/icons/comment_icon.ts +++ b/core/icons/comment_icon.ts @@ -199,7 +199,6 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { setBubbleLocation(location: Coordinate) { this.bubbleLocation = location; this.textInputBubble?.moveDuringDrag(location); - this.textBubble?.moveDuringDrag(location); } /** diff --git a/core/interfaces/i_comment_icon.ts b/core/interfaces/i_comment_icon.ts index 52e5502902a..05f86f40ff9 100644 --- a/core/interfaces/i_comment_icon.ts +++ b/core/interfaces/i_comment_icon.ts @@ -6,8 +6,8 @@ import {CommentState} from '../icons/comment_icon.js'; import {IconType} from '../icons/icon_types.js'; -import {Size} from '../utils/size.js'; import {Coordinate} from '../utils/coordinate.js'; +import {Size} from '../utils/size.js'; import {IHasBubble, hasBubble} from './i_has_bubble.js'; import {IIcon, isIcon} from './i_icon.js'; import {ISerializable, isSerializable} from './i_serializable.js'; diff --git a/core/interfaces/i_flyout_inflater.ts b/core/interfaces/i_flyout_inflater.ts index f4a3b6ee9a8..31f4c23fcbf 100644 --- a/core/interfaces/i_flyout_inflater.ts +++ b/core/interfaces/i_flyout_inflater.ts @@ -1,5 +1,5 @@ -import type {IBoundedElement} from './i_bounded_element.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; +import type {IBoundedElement} from './i_bounded_element.js'; export interface IFlyoutInflater { /** @@ -16,7 +16,7 @@ export interface IFlyoutInflater { * element, however. * @returns The newly inflated flyout element. */ - load(state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement; + load(state: object, flyoutWorkspace: WorkspaceSvg): IBoundedElement; /** * Returns the amount of spacing that should follow the element corresponding @@ -26,7 +26,7 @@ export interface IFlyoutInflater { * @param defaultGap The default gap for elements in this flyout. * @returns The gap that should follow the given element. */ - gapForElement(state: Object, defaultGap: number): number; + gapForElement(state: object, defaultGap: number): number; /** * Disposes of the given element. diff --git a/core/interfaces/i_variable_backed_parameter_model.ts b/core/interfaces/i_variable_backed_parameter_model.ts index 4fda2df4660..444deb60105 100644 --- a/core/interfaces/i_variable_backed_parameter_model.ts +++ b/core/interfaces/i_variable_backed_parameter_model.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {IVariableModel, IVariableState} from './i_variable_model.js'; import {IParameterModel} from './i_parameter_model.js'; +import type {IVariableModel, IVariableState} from './i_variable_model.js'; /** Interface for a parameter model that holds a variable model. */ export interface IVariableBackedParameterModel extends IParameterModel { diff --git a/core/label_flyout_inflater.ts b/core/label_flyout_inflater.ts index 67b02857a48..ad304a9a634 100644 --- a/core/label_flyout_inflater.ts +++ b/core/label_flyout_inflater.ts @@ -4,12 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; -import type {IBoundedElement} from './interfaces/i_bounded_element.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; import {FlyoutButton} from './flyout_button.js'; -import {ButtonOrLabelInfo} from './utils/toolbox.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import * as registry from './registry.js'; +import {ButtonOrLabelInfo} from './utils/toolbox.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; /** * Class responsible for creating labels for flyouts. @@ -22,7 +22,7 @@ export class LabelFlyoutInflater implements IFlyoutInflater { * @param flyoutWorkspace The workspace to create the label on. * @returns A FlyoutButton configured as a label. */ - load(state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { + load(state: object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { const label = new FlyoutButton( flyoutWorkspace, flyoutWorkspace.targetWorkspace!, @@ -40,7 +40,7 @@ export class LabelFlyoutInflater implements IFlyoutInflater { * @param defaultGap The default spacing for flyout items. * @returns The amount of space that should follow this label. */ - gapForElement(state: Object, defaultGap: number): number { + gapForElement(state: object, defaultGap: number): number { return defaultGap; } diff --git a/core/names.ts b/core/names.ts index 9976da224d2..db7486f719e 100644 --- a/core/names.ts +++ b/core/names.ts @@ -11,12 +11,12 @@ */ // Former goog.module ID: Blockly.Names -import {Msg} from './msg.js'; import type {IVariableMap} from './interfaces/i_variable_map.js'; import type { IVariableModel, IVariableState, } from './interfaces/i_variable_model.js'; +import {Msg} from './msg.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; diff --git a/core/renderers/common/renderer.ts b/core/renderers/common/renderer.ts index f223cffa09a..01de1f877f8 100644 --- a/core/renderers/common/renderer.ts +++ b/core/renderers/common/renderer.ts @@ -13,7 +13,6 @@ import {ConnectionType} from '../../connection_type.js'; import type {IRegistrable} from '../../interfaces/i_registrable.js'; import type {Marker} from '../../keyboard_nav/marker.js'; import type {BlockStyle, Theme} from '../../theme.js'; -import * as deprecation from '../../utils/deprecation.js'; import type {WorkspaceSvg} from '../../workspace_svg.js'; import {ConstantProvider} from './constants.js'; import {Drawer} from './drawer.js'; diff --git a/core/renderers/zelos/renderer.ts b/core/renderers/zelos/renderer.ts index fb46921d6cb..c880ce9f80b 100644 --- a/core/renderers/zelos/renderer.ts +++ b/core/renderers/zelos/renderer.ts @@ -9,7 +9,6 @@ import type {BlockSvg} from '../../block_svg.js'; import type {Marker} from '../../keyboard_nav/marker.js'; import type {BlockStyle} from '../../theme.js'; -import * as deprecation from '../../utils/deprecation.js'; import type {WorkspaceSvg} from '../../workspace_svg.js'; import * as blockRendering from '../common/block_rendering.js'; import type {RenderInfo as BaseRenderInfo} from '../common/info.js'; diff --git a/core/separator_flyout_inflater.ts b/core/separator_flyout_inflater.ts index 5ed02aeb978..8c0acf2f5c5 100644 --- a/core/separator_flyout_inflater.ts +++ b/core/separator_flyout_inflater.ts @@ -4,12 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; -import type {IBoundedElement} from './interfaces/i_bounded_element.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; -import type {SeparatorInfo} from './utils/toolbox.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import * as registry from './registry.js'; +import type {SeparatorInfo} from './utils/toolbox.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; /** * Class responsible for creating separators for flyouts. @@ -33,7 +33,7 @@ export class SeparatorFlyoutInflater implements IFlyoutInflater { * @param flyoutWorkspace The workspace the separator belongs to. * @returns A newly created FlyoutSeparator. */ - load(_state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { + load(_state: object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { const flyoutAxis = flyoutWorkspace.targetWorkspace?.getFlyout() ?.horizontalLayout ? SeparatorAxis.X @@ -48,7 +48,7 @@ export class SeparatorFlyoutInflater implements IFlyoutInflater { * @param defaultGap The default spacing for flyout items. * @returns The desired size of the separator. */ - gapForElement(state: Object, defaultGap: number): number { + gapForElement(state: object, defaultGap: number): number { const separatorState = state as SeparatorInfo; const newGap = parseInt(String(separatorState['gap'])); return newGap ?? defaultGap; diff --git a/core/serialization/blocks.ts b/core/serialization/blocks.ts index e729f682693..0c4f06c5936 100644 --- a/core/serialization/blocks.ts +++ b/core/serialization/blocks.ts @@ -36,7 +36,6 @@ import * as priorities from './priorities.js'; import * as serializationRegistry from './registry.js'; // TODO(#5160): Remove this once lint is fixed. -/* eslint-disable no-use-before-define */ /** * Represents the state of a connection. diff --git a/core/serialization/variables.ts b/core/serialization/variables.ts index 31c5eb1de97..d9c266fb834 100644 --- a/core/serialization/variables.ts +++ b/core/serialization/variables.ts @@ -8,9 +8,9 @@ import type {ISerializer} from '../interfaces/i_serializer.js'; import type {IVariableState} from '../interfaces/i_variable_model.js'; +import * as registry from '../registry.js'; import type {Workspace} from '../workspace.js'; import * as priorities from './priorities.js'; -import * as registry from '../registry.js'; import * as serializationRegistry from './registry.js'; /** diff --git a/core/variable_model.ts b/core/variable_model.ts index 9c959f4f36d..4cd16a9c321 100644 --- a/core/variable_model.ts +++ b/core/variable_model.ts @@ -14,6 +14,7 @@ // Unused import preserved for side-effects. Remove if unneeded. import './events/events_var_create.js'; +import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import * as registry from './registry.js'; @@ -138,7 +139,7 @@ export class VariableModel implements IVariableModel { state['id'], ); workspace.getVariableMap().addVariable(variable); - eventUtils.fire(new (eventUtils.get(eventUtils.VAR_CREATE))(variable)); + eventUtils.fire(new (eventUtils.get(EventType.VAR_CREATE))(variable)); } } diff --git a/core/variables.ts b/core/variables.ts index a7f571fa412..75f0c3cf88d 100644 --- a/core/variables.ts +++ b/core/variables.ts @@ -11,9 +11,9 @@ import {Blocks} from './blocks.js'; import * as dialog from './dialog.js'; import {isLegacyProcedureDefBlock} from './interfaces/i_legacy_procedure_blocks.js'; import {isVariableBackedParameterModel} from './interfaces/i_variable_backed_parameter_model.js'; +import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import {Msg} from './msg.js'; import * as utilsXml from './utils/xml.js'; -import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import type {Workspace} from './workspace.js'; import type {WorkspaceSvg} from './workspace_svg.js'; diff --git a/core/xml.ts b/core/xml.ts index dad3116c93f..a1aaa81142b 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -17,15 +17,15 @@ import * as eventUtils from './events/utils.js'; import type {Field} from './field.js'; import {IconType} from './icons/icon_types.js'; import {inputTypes} from './inputs/input_types.js'; +import type { + IVariableModel, + IVariableState, +} from './interfaces/i_variable_model.js'; import * as renderManagement from './render_management.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import {Size} from './utils/size.js'; import * as utilsXml from './utils/xml.js'; -import type { - IVariableModel, - IVariableState, -} from './interfaces/i_variable_model.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; import {WorkspaceSvg} from './workspace_svg.js'; From aad5339c01291823d463f36dec1f98e151c2f3bb Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 4 Dec 2024 12:18:08 -0800 Subject: [PATCH 082/222] fix: Fix variable type change test. --- core/events/events.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/events/events.ts b/core/events/events.ts index ab260393828..ae3b9e6b21f 100644 --- a/core/events/events.ts +++ b/core/events/events.ts @@ -48,6 +48,7 @@ export {VarBase, VarBaseJson} from './events_var_base.js'; export {VarCreate, VarCreateJson} from './events_var_create.js'; export {VarDelete, VarDeleteJson} from './events_var_delete.js'; export {VarRename, VarRenameJson} from './events_var_rename.js'; +export {VarTypeChange, VarTypeChangeJson} from './events_var_type_change.js'; export {ViewportChange, ViewportChangeJson} from './events_viewport.js'; export {FinishedLoading} from './workspace_events.js'; From d2c2d6b554adb6f72cf70fe6b7a4112944e9040a Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 4 Dec 2024 12:44:26 -0800 Subject: [PATCH 083/222] release: Update version number to 12.0.0-beta.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bb43adcf705..b1288c49a8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockly", - "version": "12.0.0", + "version": "12.0.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "12.0.0", + "version": "12.0.0-beta.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 9469fc5d329..8e10e21638b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "12.0.0", + "version": "12.0.0-beta.0", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly" From 54ebfb7a0e00c870b1a96ef57d1e0e705c28968c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 6 Jan 2025 10:52:02 -0800 Subject: [PATCH 084/222] fix: Fix unsafe cast in Input.setVisible(). (#8695) --- core/inputs/input.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/core/inputs/input.ts b/core/inputs/input.ts index 0907bf44939..f8783aea35f 100644 --- a/core/inputs/input.ts +++ b/core/inputs/input.ts @@ -20,7 +20,7 @@ import type {Connection} from '../connection.js'; import type {ConnectionType} from '../connection_type.js'; import type {Field} from '../field.js'; import * as fieldRegistry from '../field_registry.js'; -import type {RenderedConnection} from '../rendered_connection.js'; +import {RenderedConnection} from '../rendered_connection.js'; import {Align} from './align.js'; import {inputTypes} from './input_types.js'; @@ -181,15 +181,14 @@ export class Input { for (let y = 0, field; (field = this.fieldRow[y]); y++) { field.setVisible(visible); } - if (this.connection) { - const renderedConnection = this.connection as RenderedConnection; + if (this.connection && this.connection instanceof RenderedConnection) { // Has a connection. if (visible) { - renderList = renderedConnection.startTrackingAll(); + renderList = this.connection.startTrackingAll(); } else { - renderedConnection.stopTrackingAll(); + this.connection.stopTrackingAll(); } - const child = renderedConnection.targetBlock(); + const child = this.connection.targetBlock(); if (child) { child.getSvgRoot().style.display = visible ? 'block' : 'none'; } From eeef2edf34d488752e0988e17cb72e35d1b3d56e Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 6 Jan 2025 10:53:45 -0800 Subject: [PATCH 085/222] chore!: Fix warnings when generating docs. (#8660) --- api-extractor.json | 5 +++++ core/block.ts | 5 ++--- core/connection.ts | 2 +- core/events/events_var_delete.ts | 2 -- core/events/events_var_rename.ts | 2 -- core/interfaces/i_metrics_manager.ts | 2 +- core/interfaces/i_rendered_element.ts | 5 +---- core/menu.ts | 4 +--- core/metrics_manager.ts | 2 +- core/renderers/common/renderer.ts | 2 +- core/workspace_svg.ts | 1 + 11 files changed, 14 insertions(+), 18 deletions(-) diff --git a/api-extractor.json b/api-extractor.json index 6c599d255b4..66414503a29 100644 --- a/api-extractor.json +++ b/api-extractor.json @@ -352,6 +352,11 @@ // Needs investigation. "ae-forgotten-export": { "logLevel": "none" + }, + + // We don't prefix our internal APIs with underscores. + "ae-internal-missing-underscore": { + "logLevel": "none" } }, diff --git a/core/block.ts b/core/block.ts index 9c68bc4cded..b5eff8311bc 100644 --- a/core/block.ts +++ b/core/block.ts @@ -1411,7 +1411,7 @@ export class Block implements IASTNodeLocation { return this.disabledReasons.size === 0; } - /** @deprecated v11 - Get whether the block is manually disabled. */ + /** @deprecated v11 - Get or sets whether the block is manually disabled. */ private get disabled(): boolean { deprecation.warn( 'disabled', @@ -1422,7 +1422,6 @@ export class Block implements IASTNodeLocation { return this.hasDisabledReason(constants.MANUALLY_DISABLED); } - /** @deprecated v11 - Set whether the block is manually disabled. */ private set disabled(value: boolean) { deprecation.warn( 'disabled', @@ -2519,7 +2518,7 @@ export class Block implements IASTNodeLocation { * * Intended to on be used in console logs and errors. If you need a string * that uses the user's native language (including block text, field values, - * and child blocks), use [toString()]{@link Block#toString}. + * and child blocks), use {@link (Block:class).toString | toString()}. * * @returns The description. */ diff --git a/core/connection.ts b/core/connection.ts index 9cc2c28a923..039d8822c01 100644 --- a/core/connection.ts +++ b/core/connection.ts @@ -485,7 +485,7 @@ export class Connection implements IASTNodeLocationWithBlock { * * Headless configurations (the default) do not have neighboring connection, * and always return an empty list (the default). - * {@link RenderedConnection#neighbours} overrides this behavior with a list + * {@link (RenderedConnection:class).neighbours} overrides this behavior with a list * computed from the rendered positioning. * * @param _maxLimit The maximum radius to another connection. diff --git a/core/events/events_var_delete.ts b/core/events/events_var_delete.ts index 8663eb398c5..225459c44c7 100644 --- a/core/events/events_var_delete.ts +++ b/core/events/events_var_delete.ts @@ -18,8 +18,6 @@ import {EventType} from './type.js'; /** * Notifies listeners that a variable model has been deleted. - * - * @class */ export class VarDelete extends VarBase { override type = EventType.VAR_DELETE; diff --git a/core/events/events_var_rename.ts b/core/events/events_var_rename.ts index 26c272c7b0f..23a0a17cdc2 100644 --- a/core/events/events_var_rename.ts +++ b/core/events/events_var_rename.ts @@ -18,8 +18,6 @@ import {EventType} from './type.js'; /** * Notifies listeners that a variable model was renamed. - * - * @class */ export class VarRename extends VarBase { override type = EventType.VAR_RENAME; diff --git a/core/interfaces/i_metrics_manager.ts b/core/interfaces/i_metrics_manager.ts index bb4d54da440..6fc0d080cc2 100644 --- a/core/interfaces/i_metrics_manager.ts +++ b/core/interfaces/i_metrics_manager.ts @@ -63,7 +63,7 @@ export interface IMetricsManager { * Gets the width, height and position of the toolbox on the workspace in * pixel coordinates. Returns 0 for the width and height if the workspace has * a simple toolbox instead of a category toolbox. To get the width and height - * of a simple toolbox, see {@link IMetricsManager#getFlyoutMetrics}. + * of a simple toolbox, see {@link IMetricsManager.getFlyoutMetrics}. * * @returns The object with the width, height and position of the toolbox. */ diff --git a/core/interfaces/i_rendered_element.ts b/core/interfaces/i_rendered_element.ts index 7e6981ca6b1..fe9460c7f6a 100644 --- a/core/interfaces/i_rendered_element.ts +++ b/core/interfaces/i_rendered_element.ts @@ -4,18 +4,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** @internal */ export interface IRenderedElement { /** - * @returns The root SVG element of htis rendered element. + * @returns The root SVG element of this rendered element. */ getSvgRoot(): SVGElement; } /** * @returns True if the given object is an IRenderedElement. - * - * @internal */ export function isRenderedElement(obj: any): obj is IRenderedElement { return obj['getSvgRoot'] !== undefined; diff --git a/core/menu.ts b/core/menu.ts index f01c1edfb63..ec9aae571c6 100644 --- a/core/menu.ts +++ b/core/menu.ts @@ -389,9 +389,7 @@ export class Menu { // Keyboard events. /** - * Attempts to handle a keyboard event, if the menu item is enabled, by - * calling - * {@link Menu#handleKeyEventInternal_}. + * Attempts to handle a keyboard event. * * @param e Key event to handle. */ diff --git a/core/metrics_manager.ts b/core/metrics_manager.ts index 62a2614b617..a8470462ce3 100644 --- a/core/metrics_manager.ts +++ b/core/metrics_manager.ts @@ -76,7 +76,7 @@ export class MetricsManager implements IMetricsManager { * Gets the width, height and position of the toolbox on the workspace in * pixel coordinates. Returns 0 for the width and height if the workspace has * a simple toolbox instead of a category toolbox. To get the width and height - * of a simple toolbox, see {@link MetricsManager#getFlyoutMetrics}. + * of a simple toolbox, see {@link (MetricsManager:class).getFlyoutMetrics}. * * @returns The object with the width, height and position of the toolbox. */ diff --git a/core/renderers/common/renderer.ts b/core/renderers/common/renderer.ts index 01de1f877f8..812ddd97678 100644 --- a/core/renderers/common/renderer.ts +++ b/core/renderers/common/renderer.ts @@ -73,7 +73,7 @@ export class Renderer implements IRegistrable { /** * Create any DOM elements that this renderer needs. * If you need to create additional DOM elements, override the - * {@link ConstantProvider#createDom} method instead. + * {@link blockRendering#ConstantProvider.createDom} method instead. * * @param svg The root of the workspace's SVG. * @param theme The workspace theme object. diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 68f956b4dc4..60288573cec 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -1102,6 +1102,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** * @returns The layer manager for this workspace. + * @internal */ getLayerManager(): LayerManager | null { return this.layerManager; From 071814e9de6a5c49e32a50917b48f0a92e2a6910 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 6 Jan 2025 10:55:10 -0800 Subject: [PATCH 086/222] feat: Warn if a variable category is loaded without variable blocks. (#8704) --- core/variables.ts | 5 +++++ core/variables_dynamic.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/core/variables.ts b/core/variables.ts index 75f0c3cf88d..5d60b475b1d 100644 --- a/core/variables.ts +++ b/core/variables.ts @@ -92,6 +92,11 @@ export function allDeveloperVariables(workspace: Workspace): string[] { * @returns Array of XML elements. */ export function flyoutCategory(workspace: WorkspaceSvg): Element[] { + if (!Blocks['variables_set'] && !Blocks['variables_get']) { + console.warn( + 'There are no variable blocks, but there is a variable category.', + ); + } let xmlList = new Array(); const button = document.createElement('button'); button.setAttribute('text', '%{BKY_NEW_VARIABLE}'); diff --git a/core/variables_dynamic.ts b/core/variables_dynamic.ts index 8dc691d3a15..6722f8b49f8 100644 --- a/core/variables_dynamic.ts +++ b/core/variables_dynamic.ts @@ -76,6 +76,11 @@ export const onCreateVariableButtonClick_Colour = colourButtonClickHandler; * @returns Array of XML elements. */ export function flyoutCategory(workspace: WorkspaceSvg): Element[] { + if (!Blocks['variables_set_dynamic'] && !Blocks['variables_get_dynamic']) { + console.warn( + 'There are no dynamic variable blocks, but there is a dynamic variable category.', + ); + } let xmlList = new Array(); let button = document.createElement('button'); button.setAttribute('text', Msg['NEW_STRING_VARIABLE']); From 503cd0073f42193f16bbffbb472564993a1bd1c9 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 6 Jan 2025 11:08:17 -0800 Subject: [PATCH 087/222] refactor: Reenable workspace resizing after reflowing flyouts. (#8683) --- core/flyout_base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 54248edfd12..87aba011932 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -634,8 +634,8 @@ export abstract class Flyout } else { this.width_ = 0; } - this.workspace_.setResizesEnabled(true); this.reflow(); + this.workspace_.setResizesEnabled(true); // Listen for block change events, and reflow the flyout in response. This // accommodates e.g. resizing a non-autoclosing flyout in response to the From 956f272da0f4af071e3c9dfe5b5b2cd3bc3f3164 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 6 Jan 2025 11:30:22 -0800 Subject: [PATCH 088/222] feat: Add a generator for all fields on a block. (#8667) * feat: Add a generator for all fields on a block. * chore: Add docstring. --- core/block.ts | 78 ++++++++++++++++++------------------ core/block_svg.ts | 6 +-- core/serialization/blocks.ts | 10 ++--- core/xml.ts | 12 ++---- 4 files changed, 48 insertions(+), 58 deletions(-) diff --git a/core/block.ts b/core/block.ts index b5eff8311bc..f5683fcca46 100644 --- a/core/block.ts +++ b/core/block.ts @@ -937,10 +937,8 @@ export class Block implements IASTNodeLocation { */ setEditable(editable: boolean) { this.editable = editable; - for (let i = 0, input; (input = this.inputList[i]); i++) { - for (let j = 0, field; (field = input.fieldRow[j]); j++) { - field.updateEditable(); - } + for (const field of this.getFields()) { + field.updateEditable(); } } @@ -1107,16 +1105,27 @@ export class Block implements IASTNodeLocation { ' instead', ); } - for (let i = 0, input; (input = this.inputList[i]); i++) { - for (let j = 0, field; (field = input.fieldRow[j]); j++) { - if (field.name === name) { - return field; - } + for (const field of this.getFields()) { + if (field.name === name) { + return field; } } return null; } + /** + * Returns a generator that provides every field on the block. + * + * @yields A generator that can be used to iterate the fields on the block. + */ + *getFields(): Generator { + for (const input of this.inputList) { + for (const field of input.fieldRow) { + yield field; + } + } + } + /** * Return all variables referenced by this block. * @@ -1124,12 +1133,9 @@ export class Block implements IASTNodeLocation { */ getVars(): string[] { const vars: string[] = []; - for (let i = 0, input; (input = this.inputList[i]); i++) { - for (let j = 0, field; (field = input.fieldRow[j]); j++) { - if (field.referencesVariables()) { - // NOTE: This only applies to `FieldVariable`, a `Field` - vars.push(field.getValue() as string); - } + for (const field of this.getFields()) { + if (field.referencesVariables()) { + vars.push(field.getValue()); } } return vars; @@ -1143,17 +1149,15 @@ export class Block implements IASTNodeLocation { */ getVarModels(): IVariableModel[] { const vars = []; - for (let i = 0, input; (input = this.inputList[i]); i++) { - for (let j = 0, field; (field = input.fieldRow[j]); j++) { - if (field.referencesVariables()) { - const model = this.workspace.getVariableById( - field.getValue() as string, - ); - // Check if the variable actually exists (and isn't just a potential - // variable). - if (model) { - vars.push(model); - } + for (const field of this.getFields()) { + if (field.referencesVariables()) { + const model = this.workspace.getVariableById( + field.getValue() as string, + ); + // Check if the variable actually exists (and isn't just a potential + // variable). + if (model) { + vars.push(model); } } } @@ -1168,14 +1172,12 @@ export class Block implements IASTNodeLocation { * @internal */ updateVarName(variable: IVariableModel) { - for (let i = 0, input; (input = this.inputList[i]); i++) { - for (let j = 0, field; (field = input.fieldRow[j]); j++) { - if ( - field.referencesVariables() && - variable.getId() === field.getValue() - ) { - field.refreshVariableName(); - } + for (const field of this.getFields()) { + if ( + field.referencesVariables() && + variable.getId() === field.getValue() + ) { + field.refreshVariableName(); } } } @@ -1189,11 +1191,9 @@ export class Block implements IASTNodeLocation { * updated name. */ renameVarById(oldId: string, newId: string) { - for (let i = 0, input; (input = this.inputList[i]); i++) { - for (let j = 0, field; (field = input.fieldRow[j]); j++) { - if (field.referencesVariables() && oldId === field.getValue()) { - field.setValue(newId); - } + for (const field of this.getFields()) { + if (field.referencesVariables() && oldId === field.getValue()) { + field.setValue(newId); } } } diff --git a/core/block_svg.ts b/core/block_svg.ts index aabe51f7a87..f04da034a7f 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -911,10 +911,8 @@ export class BlockSvg icons[i].applyColour(); } - for (let x = 0, input; (input = this.inputList[x]); x++) { - for (let y = 0, field; (field = input.fieldRow[y]); y++) { - field.applyColour(); - } + for (const field of this.getFields()) { + field.applyColour(); } } diff --git a/core/serialization/blocks.ts b/core/serialization/blocks.ts index 0c4f06c5936..3696ab2f273 100644 --- a/core/serialization/blocks.ts +++ b/core/serialization/blocks.ts @@ -261,13 +261,9 @@ function saveIcons(block: Block, state: State, doFullSerialization: boolean) { */ function saveFields(block: Block, state: State, doFullSerialization: boolean) { const fields = Object.create(null); - for (let i = 0; i < block.inputList.length; i++) { - const input = block.inputList[i]; - for (let j = 0; j < input.fieldRow.length; j++) { - const field = input.fieldRow[j]; - if (field.isSerializable()) { - fields[field.name!] = field.saveState(doFullSerialization); - } + for (const field of block.getFields()) { + if (field.isSerializable()) { + fields[field.name!] = field.saveState(doFullSerialization); } } if (Object.keys(fields).length) { diff --git a/core/xml.ts b/core/xml.ts index a1aaa81142b..f4b5f66ddd2 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -168,14 +168,10 @@ function fieldToDom(field: Field): Element | null { * @param element The XML element to which the field DOM should be attached. */ function allFieldsToDom(block: Block, element: Element) { - for (let i = 0; i < block.inputList.length; i++) { - const input = block.inputList[i]; - for (let j = 0; j < input.fieldRow.length; j++) { - const field = input.fieldRow[j]; - const fieldDom = fieldToDom(field); - if (fieldDom) { - element.appendChild(fieldDom); - } + for (const field of block.getFields()) { + const fieldDom = fieldToDom(field); + if (fieldDom) { + element.appendChild(fieldDom); } } } From 151d21e50e930b11a08e36a0de2b85542da50cf2 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 7 Jan 2025 14:04:21 -0800 Subject: [PATCH 089/222] refactor: Convert renderer typecheck methods to typeguards. (#8656) * refactor: Convert renderer typecheck methods to typeguards. * chore: Revert unintended change. * chore: Format types.ts. --- core/renderers/common/drawer.ts | 20 +-- core/renderers/common/info.ts | 9 +- core/renderers/geras/drawer.ts | 2 +- core/renderers/geras/info.ts | 25 ++-- core/renderers/measurables/in_row_spacer.ts | 8 ++ core/renderers/measurables/input_row.ts | 8 +- core/renderers/measurables/jagged_edge.ts | 8 ++ core/renderers/measurables/next_connection.ts | 8 ++ .../measurables/previous_connection.ts | 8 ++ core/renderers/measurables/round_corner.ts | 8 ++ core/renderers/measurables/row.ts | 10 +- core/renderers/measurables/square_corner.ts | 8 ++ core/renderers/measurables/statement_input.ts | 8 ++ core/renderers/measurables/top_row.ts | 3 +- core/renderers/measurables/types.ts | 136 +++++++++++------- core/renderers/thrasos/info.ts | 22 ++- core/renderers/zelos/drawer.ts | 10 +- core/renderers/zelos/info.ts | 16 +-- 18 files changed, 189 insertions(+), 128 deletions(-) diff --git a/core/renderers/common/drawer.ts b/core/renderers/common/drawer.ts index 59a856011f2..09320710c51 100644 --- a/core/renderers/common/drawer.ts +++ b/core/renderers/common/drawer.ts @@ -15,7 +15,6 @@ import type {ExternalValueInput} from '../measurables/external_value_input.js'; import type {Field} from '../measurables/field.js'; import type {Icon} from '../measurables/icon.js'; import type {InlineInput} from '../measurables/inline_input.js'; -import type {PreviousConnection} from '../measurables/previous_connection.js'; import type {Row} from '../measurables/row.js'; import {Types} from '../measurables/types.js'; import type {ConstantProvider, Notch, PuzzleTab} from './constants.js'; @@ -116,13 +115,8 @@ export class Drawer { this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topLeft; } else if (Types.isRightRoundedCorner(elem)) { this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topRight; - } else if ( - Types.isPreviousConnection(elem) && - elem instanceof Connection - ) { - this.outlinePath_ += ( - (elem as PreviousConnection).shape as Notch - ).pathLeft; + } else if (Types.isPreviousConnection(elem)) { + this.outlinePath_ += (elem.shape as Notch).pathLeft; } else if (Types.isHat(elem)) { this.outlinePath_ += this.constants_.START_HAT.path; } else if (Types.isSpacer(elem)) { @@ -217,7 +211,7 @@ export class Drawer { let rightCornerYOffset = 0; let outlinePath = ''; for (let i = elems.length - 1, elem; (elem = elems[i]); i--) { - if (Types.isNextConnection(elem) && elem instanceof Connection) { + if (Types.isNextConnection(elem)) { outlinePath += (elem.shape as Notch).pathRight; } else if (Types.isLeftSquareCorner(elem)) { outlinePath += svgPaths.lineOnAxis('H', bottomRow.xPos); @@ -269,9 +263,9 @@ export class Drawer { for (let i = 0, row; (row = this.info_.rows[i]); i++) { for (let j = 0, elem; (elem = row.elements[j]); j++) { if (Types.isInlineInput(elem)) { - this.drawInlineInput_(elem as InlineInput); + this.drawInlineInput_(elem); } else if (Types.isIcon(elem) || Types.isField(elem)) { - this.layoutField_(elem as Field | Icon); + this.layoutField_(elem); } } } @@ -295,13 +289,13 @@ export class Drawer { } if (Types.isIcon(fieldInfo)) { - const icon = (fieldInfo as Icon).icon; + const icon = fieldInfo.icon; icon.setOffsetInBlock(new Coordinate(xPos, yPos)); if (this.info_.isInsertionMarker) { icon.hideForInsertionMarker(); } } else { - const svgGroup = (fieldInfo as Field).field.getSvgRoot()!; + const svgGroup = fieldInfo.field.getSvgRoot()!; svgGroup.setAttribute( 'transform', 'translate(' + xPos + ',' + yPos + ')' + scale, diff --git a/core/renderers/common/info.ts b/core/renderers/common/info.ts index 680fa57f210..f826d17e463 100644 --- a/core/renderers/common/info.ts +++ b/core/renderers/common/info.ts @@ -671,20 +671,17 @@ export class RenderInfo { return row.yPos + elem.height / 2; } if (Types.isBottomRow(row)) { - const bottomRow = row as BottomRow; - const baseline = - bottomRow.yPos + bottomRow.height - bottomRow.descenderHeight; + const baseline = row.yPos + row.height - row.descenderHeight; if (Types.isNextConnection(elem)) { return baseline + elem.height / 2; } return baseline - elem.height / 2; } if (Types.isTopRow(row)) { - const topRow = row as TopRow; if (Types.isHat(elem)) { - return topRow.capline - elem.height / 2; + return row.capline - elem.height / 2; } - return topRow.capline + elem.height / 2; + return row.capline + elem.height / 2; } return row.yPos + row.height / 2; } diff --git a/core/renderers/geras/drawer.ts b/core/renderers/geras/drawer.ts index 542b21ff93b..9d0ed829be7 100644 --- a/core/renderers/geras/drawer.ts +++ b/core/renderers/geras/drawer.ts @@ -100,7 +100,7 @@ export class Drawer extends BaseDrawer { } override drawInlineInput_(input: InlineInput) { - this.highlighter_.drawInlineInput(input as InlineInput); + this.highlighter_.drawInlineInput(input); super.drawInlineInput_(input); } diff --git a/core/renderers/geras/info.ts b/core/renderers/geras/info.ts index b9cc1c59c8c..3e1980aa0a5 100644 --- a/core/renderers/geras/info.ts +++ b/core/renderers/geras/info.ts @@ -14,13 +14,9 @@ import {StatementInput} from '../../inputs/statement_input.js'; import {ValueInput} from '../../inputs/value_input.js'; import {RenderInfo as BaseRenderInfo} from '../common/info.js'; import type {Measurable} from '../measurables/base.js'; -import type {BottomRow} from '../measurables/bottom_row.js'; import {ExternalValueInput} from '../measurables/external_value_input.js'; -import type {Field} from '../measurables/field.js'; import {InRowSpacer} from '../measurables/in_row_spacer.js'; -import type {InputRow} from '../measurables/input_row.js'; import type {Row} from '../measurables/row.js'; -import type {TopRow} from '../measurables/top_row.js'; import {Types} from '../measurables/types.js'; import type {ConstantProvider} from './constants.js'; import {InlineInput} from './measurables/inline_input.js'; @@ -150,7 +146,7 @@ export class RenderInfo extends BaseRenderInfo { override getInRowSpacing_(prev: Measurable | null, next: Measurable | null) { if (!prev) { // Between an editable field and the beginning of the row. - if (next && Types.isField(next) && (next as Field).isEditable) { + if (next && Types.isField(next) && next.isEditable) { return this.constants_.MEDIUM_PADDING; } // Inline input at the beginning of the row. @@ -167,7 +163,7 @@ export class RenderInfo extends BaseRenderInfo { // Spacing between a non-input and the end of the row or a statement input. if (!Types.isInput(prev) && (!next || Types.isStatementInput(next))) { // Between an editable field and the end of the row. - if (Types.isField(prev) && (prev as Field).isEditable) { + if (Types.isField(prev) && prev.isEditable) { return this.constants_.MEDIUM_PADDING; } // Padding at the end of an icon-only row to make the block shape clearer. @@ -208,7 +204,7 @@ export class RenderInfo extends BaseRenderInfo { // Spacing between a non-input and an input. if (!Types.isInput(prev) && next && Types.isInput(next)) { // Between an editable field and an input. - if (Types.isField(prev) && (prev as Field).isEditable) { + if (Types.isField(prev) && prev.isEditable) { if (Types.isInlineInput(next)) { return this.constants_.SMALL_PADDING; } else if (Types.isExternalInput(next)) { @@ -233,7 +229,7 @@ export class RenderInfo extends BaseRenderInfo { // Spacing between an inline input and a field. if (Types.isInlineInput(prev) && next && Types.isField(next)) { // Editable field after inline input. - if ((next as Field).isEditable) { + if (next.isEditable) { return this.constants_.MEDIUM_PADDING; } else { // Noneditable field after inline input. @@ -278,7 +274,7 @@ export class RenderInfo extends BaseRenderInfo { Types.isField(prev) && next && Types.isField(next) && - (prev as Field).isEditable === (next as Field).isEditable + prev.isEditable === next.isEditable ) { return this.constants_.LARGE_PADDING; } @@ -323,20 +319,17 @@ export class RenderInfo extends BaseRenderInfo { return row.yPos + elem.height / 2; } if (Types.isBottomRow(row)) { - const bottomRow = row as BottomRow; - const baseline = - bottomRow.yPos + bottomRow.height - bottomRow.descenderHeight; + const baseline = row.yPos + row.height - row.descenderHeight; if (Types.isNextConnection(elem)) { return baseline + elem.height / 2; } return baseline - elem.height / 2; } if (Types.isTopRow(row)) { - const topRow = row as TopRow; if (Types.isHat(elem)) { - return topRow.capline - elem.height / 2; + return row.capline - elem.height / 2; } - return topRow.capline + elem.height / 2; + return row.capline + elem.height / 2; } let result = row.yPos; @@ -370,7 +363,7 @@ export class RenderInfo extends BaseRenderInfo { rowNextRightEdges.set(row, nextRightEdge); if (Types.isInputRow(row)) { if (row.hasStatement) { - this.alignStatementRow_(row as InputRow); + this.alignStatementRow_(row); } if ( prevInput && diff --git a/core/renderers/measurables/in_row_spacer.ts b/core/renderers/measurables/in_row_spacer.ts index ec64e71a23a..d9378620cf7 100644 --- a/core/renderers/measurables/in_row_spacer.ts +++ b/core/renderers/measurables/in_row_spacer.ts @@ -15,6 +15,14 @@ import {Types} from './types.js'; * row. */ export class InRowSpacer extends Measurable { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private inRowSpacer: undefined; + /** * @param constants The rendering constants provider. * @param width The width of the spacer. diff --git a/core/renderers/measurables/input_row.ts b/core/renderers/measurables/input_row.ts index a9924246f38..869e6718f03 100644 --- a/core/renderers/measurables/input_row.ts +++ b/core/renderers/measurables/input_row.ts @@ -7,10 +7,7 @@ // Former goog.module ID: Blockly.blockRendering.InputRow import type {ConstantProvider} from '../common/constants.js'; -import {ExternalValueInput} from './external_value_input.js'; -import {InputConnection} from './input_connection.js'; import {Row} from './row.js'; -import {StatementInput} from './statement_input.js'; import {Types} from './types.js'; /** @@ -40,12 +37,11 @@ export class InputRow extends Row { for (let i = 0; i < this.elements.length; i++) { const elem = this.elements[i]; this.width += elem.width; - if (Types.isInput(elem) && elem instanceof InputConnection) { - if (Types.isStatementInput(elem) && elem instanceof StatementInput) { + if (Types.isInput(elem)) { + if (Types.isStatementInput(elem)) { connectedBlockWidths += elem.connectedBlockWidth; } else if ( Types.isExternalInput(elem) && - elem instanceof ExternalValueInput && elem.connectedBlockWidth !== 0 ) { connectedBlockWidths += diff --git a/core/renderers/measurables/jagged_edge.ts b/core/renderers/measurables/jagged_edge.ts index daca2512118..982e2b3530c 100644 --- a/core/renderers/measurables/jagged_edge.ts +++ b/core/renderers/measurables/jagged_edge.ts @@ -15,6 +15,14 @@ import {Types} from './types.js'; * collapsed block takes up during rendering. */ export class JaggedEdge extends Measurable { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private jaggedEdge: undefined; + /** * @param constants The rendering constants provider. */ diff --git a/core/renderers/measurables/next_connection.ts b/core/renderers/measurables/next_connection.ts index ea22001ed53..c10a26904bc 100644 --- a/core/renderers/measurables/next_connection.ts +++ b/core/renderers/measurables/next_connection.ts @@ -16,6 +16,14 @@ import {Types} from './types.js'; * up during rendering. */ export class NextConnection extends Connection { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private nextConnection: undefined; + /** * @param constants The rendering constants provider. * @param connectionModel The connection object on the block that this diff --git a/core/renderers/measurables/previous_connection.ts b/core/renderers/measurables/previous_connection.ts index 1314eb6a45d..30944766c48 100644 --- a/core/renderers/measurables/previous_connection.ts +++ b/core/renderers/measurables/previous_connection.ts @@ -16,6 +16,14 @@ import {Types} from './types.js'; * up during rendering. */ export class PreviousConnection extends Connection { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private previousConnection: undefined; + /** * @param constants The rendering constants provider. * @param connectionModel The connection object on the block that this diff --git a/core/renderers/measurables/round_corner.ts b/core/renderers/measurables/round_corner.ts index 60bbed70784..02c90546e1d 100644 --- a/core/renderers/measurables/round_corner.ts +++ b/core/renderers/measurables/round_corner.ts @@ -15,6 +15,14 @@ import {Types} from './types.js'; * during rendering. */ export class RoundCorner extends Measurable { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private roundCorner: undefined; + /** * @param constants The rendering constants provider. * @param opt_position The position of this corner. diff --git a/core/renderers/measurables/row.ts b/core/renderers/measurables/row.ts index 613ec6ace74..bc4707e83af 100644 --- a/core/renderers/measurables/row.ts +++ b/core/renderers/measurables/row.ts @@ -127,7 +127,7 @@ export class Row { for (let i = this.elements.length - 1; i >= 0; i--) { const elem = this.elements[i]; if (Types.isInput(elem)) { - return elem as InputConnection; + return elem; } } return null; @@ -166,8 +166,8 @@ export class Row { getFirstSpacer(): InRowSpacer | null { for (let i = 0; i < this.elements.length; i++) { const elem = this.elements[i]; - if (Types.isSpacer(elem)) { - return elem as InRowSpacer; + if (Types.isInRowSpacer(elem)) { + return elem; } } return null; @@ -181,8 +181,8 @@ export class Row { getLastSpacer(): InRowSpacer | null { for (let i = this.elements.length - 1; i >= 0; i--) { const elem = this.elements[i]; - if (Types.isSpacer(elem)) { - return elem as InRowSpacer; + if (Types.isInRowSpacer(elem)) { + return elem; } } return null; diff --git a/core/renderers/measurables/square_corner.ts b/core/renderers/measurables/square_corner.ts index 29749ac057d..054e148be23 100644 --- a/core/renderers/measurables/square_corner.ts +++ b/core/renderers/measurables/square_corner.ts @@ -15,6 +15,14 @@ import {Types} from './types.js'; * during rendering. */ export class SquareCorner extends Measurable { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private squareCorner: undefined; + /** * @param constants The rendering constants provider. * @param opt_position The position of this corner. diff --git a/core/renderers/measurables/statement_input.ts b/core/renderers/measurables/statement_input.ts index 91fe5b64a45..b0b527d36dd 100644 --- a/core/renderers/measurables/statement_input.ts +++ b/core/renderers/measurables/statement_input.ts @@ -16,6 +16,14 @@ import {Types} from './types.js'; * during rendering */ export class StatementInput extends InputConnection { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private statementInput: undefined; + /** * @param constants The rendering constants provider. * @param input The statement input to measure and store information for. diff --git a/core/renderers/measurables/top_row.ts b/core/renderers/measurables/top_row.ts index b87ce4ad753..f1e7794806d 100644 --- a/core/renderers/measurables/top_row.ts +++ b/core/renderers/measurables/top_row.ts @@ -8,7 +8,6 @@ import type {BlockSvg} from '../../block_svg.js'; import type {ConstantProvider} from '../common/constants.js'; -import {Hat} from './hat.js'; import type {PreviousConnection} from './previous_connection.js'; import {Row} from './row.js'; import {Types} from './types.js'; @@ -85,7 +84,7 @@ export class TopRow extends Row { const elem = this.elements[i]; width += elem.width; if (!Types.isSpacer(elem)) { - if (Types.isHat(elem) && elem instanceof Hat) { + if (Types.isHat(elem)) { ascenderHeight = Math.max(ascenderHeight, elem.ascenderHeight); } else { height = Math.max(height, elem.height); diff --git a/core/renderers/measurables/types.ts b/core/renderers/measurables/types.ts index a145b156303..99de339f1b1 100644 --- a/core/renderers/measurables/types.ts +++ b/core/renderers/measurables/types.ts @@ -7,7 +7,24 @@ // Former goog.module ID: Blockly.blockRendering.Types import type {Measurable} from './base.js'; +import type {BottomRow} from './bottom_row.js'; +import type {ExternalValueInput} from './external_value_input.js'; +import type {Field} from './field.js'; +import type {Hat} from './hat.js'; +import type {Icon} from './icon.js'; +import type {InRowSpacer} from './in_row_spacer.js'; +import type {InlineInput} from './inline_input.js'; +import type {InputConnection} from './input_connection.js'; +import type {InputRow} from './input_row.js'; +import type {JaggedEdge} from './jagged_edge.js'; +import type {NextConnection} from './next_connection.js'; +import type {PreviousConnection} from './previous_connection.js'; +import type {RoundCorner} from './round_corner.js'; import type {Row} from './row.js'; +import type {SpacerRow} from './spacer_row.js'; +import type {SquareCorner} from './square_corner.js'; +import type {StatementInput} from './statement_input.js'; +import type {TopRow} from './top_row.js'; /** * Types of rendering elements. @@ -82,8 +99,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a field. */ - isField(elem: Measurable): number { - return elem.type & this.FIELD; + isField(elem: Measurable): elem is Field { + return (elem.type & this.FIELD) >= 1; } /** @@ -92,8 +109,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a hat. */ - isHat(elem: Measurable): number { - return elem.type & this.HAT; + isHat(elem: Measurable): elem is Hat { + return (elem.type & this.HAT) >= 1; } /** @@ -102,8 +119,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about an icon. */ - isIcon(elem: Measurable): number { - return elem.type & this.ICON; + isIcon(elem: Measurable): elem is Icon { + return (elem.type & this.ICON) >= 1; } /** @@ -112,8 +129,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a spacer. */ - isSpacer(elem: Measurable | Row): number { - return elem.type & this.SPACER; + isSpacer(elem: Measurable | Row): elem is SpacerRow | InRowSpacer { + return (elem.type & this.SPACER) >= 1; } /** @@ -122,8 +139,18 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about an in-row spacer. */ - isInRowSpacer(elem: Measurable): number { - return elem.type & this.IN_ROW_SPACER; + isInRowSpacer(elem: Measurable): elem is InRowSpacer { + return (elem.type & this.IN_ROW_SPACER) >= 1; + } + + /** + * Whether a row is a spacer row. + * + * @param row The row to check. + * @returns True if the row is a spacer row. + */ + isSpacerRow(row: Row): row is SpacerRow { + return (row.type & this.BETWEEN_ROW_SPACER) >= 1; } /** @@ -132,8 +159,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about an input. */ - isInput(elem: Measurable): number { - return elem.type & this.INPUT; + isInput(elem: Measurable): elem is InputConnection { + return (elem.type & this.INPUT) >= 1; } /** @@ -142,8 +169,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about an external input. */ - isExternalInput(elem: Measurable): number { - return elem.type & this.EXTERNAL_VALUE_INPUT; + isExternalInput(elem: Measurable): elem is ExternalValueInput { + return (elem.type & this.EXTERNAL_VALUE_INPUT) >= 1; } /** @@ -152,8 +179,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about an inline input. */ - isInlineInput(elem: Measurable): number { - return elem.type & this.INLINE_INPUT; + isInlineInput(elem: Measurable): elem is InlineInput { + return (elem.type & this.INLINE_INPUT) >= 1; } /** @@ -162,8 +189,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a statement input. */ - isStatementInput(elem: Measurable): number { - return elem.type & this.STATEMENT_INPUT; + isStatementInput(elem: Measurable): elem is StatementInput { + return (elem.type & this.STATEMENT_INPUT) >= 1; } /** @@ -172,8 +199,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a previous connection. */ - isPreviousConnection(elem: Measurable): number { - return elem.type & this.PREVIOUS_CONNECTION; + isPreviousConnection(elem: Measurable): elem is PreviousConnection { + return (elem.type & this.PREVIOUS_CONNECTION) >= 1; } /** @@ -182,8 +209,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a next connection. */ - isNextConnection(elem: Measurable): number { - return elem.type & this.NEXT_CONNECTION; + isNextConnection(elem: Measurable): elem is NextConnection { + return (elem.type & this.NEXT_CONNECTION) >= 1; } /** @@ -194,8 +221,17 @@ class TypesContainer { * @returns 1 if the object stores information about a previous or next * connection. */ - isPreviousOrNextConnection(elem: Measurable): number { - return elem.type & (this.PREVIOUS_CONNECTION | this.NEXT_CONNECTION); + isPreviousOrNextConnection( + elem: Measurable, + ): elem is PreviousConnection | NextConnection { + return this.isPreviousConnection(elem) || this.isNextConnection(elem); + } + + isRoundCorner(elem: Measurable): elem is RoundCorner { + return ( + (elem.type & this.LEFT_ROUND_CORNER) >= 1 || + (elem.type & this.RIGHT_ROUND_CORNER) >= 1 + ); } /** @@ -204,8 +240,10 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a left round corner. */ - isLeftRoundedCorner(elem: Measurable): number { - return elem.type & this.LEFT_ROUND_CORNER; + isLeftRoundedCorner(elem: Measurable): boolean { + return ( + this.isRoundCorner(elem) && (elem.type & this.LEFT_ROUND_CORNER) >= 1 + ); } /** @@ -214,8 +252,10 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a right round corner. */ - isRightRoundedCorner(elem: Measurable): number { - return elem.type & this.RIGHT_ROUND_CORNER; + isRightRoundedCorner(elem: Measurable): boolean { + return ( + this.isRoundCorner(elem) && (elem.type & this.RIGHT_ROUND_CORNER) >= 1 + ); } /** @@ -224,8 +264,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a left square corner. */ - isLeftSquareCorner(elem: Measurable): number { - return elem.type & this.LEFT_SQUARE_CORNER; + isLeftSquareCorner(elem: Measurable): boolean { + return (elem.type & this.LEFT_SQUARE_CORNER) >= 1; } /** @@ -234,8 +274,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a right square corner. */ - isRightSquareCorner(elem: Measurable): number { - return elem.type & this.RIGHT_SQUARE_CORNER; + isRightSquareCorner(elem: Measurable): boolean { + return (elem.type & this.RIGHT_SQUARE_CORNER) >= 1; } /** @@ -244,8 +284,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a corner. */ - isCorner(elem: Measurable): number { - return elem.type & this.CORNER; + isCorner(elem: Measurable): elem is SquareCorner | RoundCorner { + return (elem.type & this.CORNER) >= 1; } /** @@ -254,8 +294,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a jagged edge. */ - isJaggedEdge(elem: Measurable): number { - return elem.type & this.JAGGED_EDGE; + isJaggedEdge(elem: Measurable): elem is JaggedEdge { + return (elem.type & this.JAGGED_EDGE) >= 1; } /** @@ -264,8 +304,8 @@ class TypesContainer { * @param row The row to check. * @returns 1 if the object stores information about a row. */ - isRow(row: Row): number { - return row.type & this.ROW; + isRow(row: Row): row is Row { + return (row.type & this.ROW) >= 1; } /** @@ -274,8 +314,8 @@ class TypesContainer { * @param row The row to check. * @returns 1 if the object stores information about a between-row spacer. */ - isBetweenRowSpacer(row: Row): number { - return row.type & this.BETWEEN_ROW_SPACER; + isBetweenRowSpacer(row: Row): row is SpacerRow { + return (row.type & this.BETWEEN_ROW_SPACER) >= 1; } /** @@ -284,8 +324,8 @@ class TypesContainer { * @param row The row to check. * @returns 1 if the object stores information about a top row. */ - isTopRow(row: Row): number { - return row.type & this.TOP_ROW; + isTopRow(row: Row): row is TopRow { + return (row.type & this.TOP_ROW) >= 1; } /** @@ -294,8 +334,8 @@ class TypesContainer { * @param row The row to check. * @returns 1 if the object stores information about a bottom row. */ - isBottomRow(row: Row): number { - return row.type & this.BOTTOM_ROW; + isBottomRow(row: Row): row is BottomRow { + return (row.type & this.BOTTOM_ROW) >= 1; } /** @@ -304,8 +344,8 @@ class TypesContainer { * @param row The row to check. * @returns 1 if the object stores information about a top or bottom row. */ - isTopOrBottomRow(row: Row): number { - return row.type & (this.TOP_ROW | this.BOTTOM_ROW); + isTopOrBottomRow(row: Row): row is TopRow | BottomRow { + return this.isTopRow(row) || this.isBottomRow(row); } /** @@ -314,8 +354,8 @@ class TypesContainer { * @param row The row to check. * @returns 1 if the object stores information about an input row. */ - isInputRow(row: Row): number { - return row.type & this.INPUT_ROW; + isInputRow(row: Row): row is InputRow { + return (row.type & this.INPUT_ROW) >= 1; } } diff --git a/core/renderers/thrasos/info.ts b/core/renderers/thrasos/info.ts index 23772a9af0e..3c8c19f9478 100644 --- a/core/renderers/thrasos/info.ts +++ b/core/renderers/thrasos/info.ts @@ -9,11 +9,8 @@ import type {BlockSvg} from '../../block_svg.js'; import {RenderInfo as BaseRenderInfo} from '../common/info.js'; import type {Measurable} from '../measurables/base.js'; -import type {BottomRow} from '../measurables/bottom_row.js'; -import type {Field} from '../measurables/field.js'; import {InRowSpacer} from '../measurables/in_row_spacer.js'; import type {Row} from '../measurables/row.js'; -import type {TopRow} from '../measurables/top_row.js'; import {Types} from '../measurables/types.js'; import type {Renderer} from './renderer.js'; @@ -94,7 +91,7 @@ export class RenderInfo extends BaseRenderInfo { override getInRowSpacing_(prev: Measurable | null, next: Measurable | null) { if (!prev) { // Between an editable field and the beginning of the row. - if (next && Types.isField(next) && (next as Field).isEditable) { + if (next && Types.isField(next) && next.isEditable) { return this.constants_.MEDIUM_PADDING; } // Inline input at the beginning of the row. @@ -111,7 +108,7 @@ export class RenderInfo extends BaseRenderInfo { // Spacing between a non-input and the end of the row. if (!Types.isInput(prev) && !next) { // Between an editable field and the end of the row. - if (Types.isField(prev) && (prev as Field).isEditable) { + if (Types.isField(prev) && prev.isEditable) { return this.constants_.MEDIUM_PADDING; } // Padding at the end of an icon-only row to make the block shape clearer. @@ -151,7 +148,7 @@ export class RenderInfo extends BaseRenderInfo { // Spacing between a non-input and an input. if (!Types.isInput(prev) && next && Types.isInput(next)) { // Between an editable field and an input. - if (Types.isField(prev) && (prev as Field).isEditable) { + if (Types.isField(prev) && prev.isEditable) { if (Types.isInlineInput(next)) { return this.constants_.SMALL_PADDING; } else if (Types.isExternalInput(next)) { @@ -177,7 +174,7 @@ export class RenderInfo extends BaseRenderInfo { // Spacing between an inline input and a field. if (Types.isInlineInput(prev) && next && Types.isField(next)) { // Editable field after inline input. - if ((next as Field).isEditable) { + if (next.isEditable) { return this.constants_.MEDIUM_PADDING; } else { // Noneditable field after inline input. @@ -205,7 +202,7 @@ export class RenderInfo extends BaseRenderInfo { Types.isField(prev) && next && Types.isField(next) && - (prev as Field).isEditable === (next as Field).isEditable + prev.isEditable === next.isEditable ) { return this.constants_.LARGE_PADDING; } @@ -247,20 +244,17 @@ export class RenderInfo extends BaseRenderInfo { return row.yPos + elem.height / 2; } if (Types.isBottomRow(row)) { - const bottomRow = row as BottomRow; - const baseline = - bottomRow.yPos + bottomRow.height - bottomRow.descenderHeight; + const baseline = row.yPos + row.height - row.descenderHeight; if (Types.isNextConnection(elem)) { return baseline + elem.height / 2; } return baseline - elem.height / 2; } if (Types.isTopRow(row)) { - const topRow = row as TopRow; if (Types.isHat(elem)) { - return topRow.capline - elem.height / 2; + return row.capline - elem.height / 2; } - return topRow.capline + elem.height / 2; + return row.capline + elem.height / 2; } let result = row.yPos; diff --git a/core/renderers/zelos/drawer.ts b/core/renderers/zelos/drawer.ts index e5b91c1e607..5cc52c0cbb2 100644 --- a/core/renderers/zelos/drawer.ts +++ b/core/renderers/zelos/drawer.ts @@ -15,7 +15,6 @@ import {Connection} from '../measurables/connection.js'; import type {InlineInput} from '../measurables/inline_input.js'; import {OutputConnection} from '../measurables/output_connection.js'; import type {Row} from '../measurables/row.js'; -import type {SpacerRow} from '../measurables/spacer_row.js'; import {Types} from '../measurables/types.js'; import type {InsideCorners} from './constants.js'; import type {RenderInfo} from './info.js'; @@ -96,20 +95,19 @@ export class Drawer extends BaseDrawer { return; } if (Types.isSpacer(row)) { - const spacerRow = row as SpacerRow; - const precedesStatement = spacerRow.precedesStatement; - const followsStatement = spacerRow.followsStatement; + const precedesStatement = row.precedesStatement; + const followsStatement = row.followsStatement; if (precedesStatement || followsStatement) { const insideCorners = this.constants_.INSIDE_CORNERS as InsideCorners; const cornerHeight = insideCorners.rightHeight; const remainingHeight = - spacerRow.height - (precedesStatement ? cornerHeight : 0); + row.height - (precedesStatement ? cornerHeight : 0); const bottomRightPath = followsStatement ? insideCorners.pathBottomRight : ''; const verticalPath = remainingHeight > 0 - ? svgPaths.lineOnAxis('V', spacerRow.yPos + remainingHeight) + ? svgPaths.lineOnAxis('V', row.yPos + remainingHeight) : ''; const topRightPath = precedesStatement ? insideCorners.pathTopRight diff --git a/core/renderers/zelos/info.ts b/core/renderers/zelos/info.ts index dd3702fe5d1..5c507c33a79 100644 --- a/core/renderers/zelos/info.ts +++ b/core/renderers/zelos/info.ts @@ -20,7 +20,6 @@ import {RenderInfo as BaseRenderInfo} from '../common/info.js'; import type {Measurable} from '../measurables/base.js'; import {Field} from '../measurables/field.js'; import {InRowSpacer} from '../measurables/in_row_spacer.js'; -import {InputConnection} from '../measurables/input_connection.js'; import type {Row} from '../measurables/row.js'; import type {SpacerRow} from '../measurables/spacer_row.js'; import {Types} from '../measurables/types.js'; @@ -207,9 +206,8 @@ export class RenderInfo extends BaseRenderInfo { } // Top and bottom rows act as a spacer so we don't need any extra padding. if (Types.isTopRow(prev)) { - const topRow = prev as TopRow; if ( - !topRow.hasPreviousConnection && + !prev.hasPreviousConnection && (!this.outputConnection || this.hasStatementInput) ) { return Math.abs( @@ -219,7 +217,6 @@ export class RenderInfo extends BaseRenderInfo { return this.constants_.NO_PADDING; } if (Types.isBottomRow(next)) { - const bottomRow = next as BottomRow; if (!this.outputConnection) { const topHeight = Math.max( @@ -230,7 +227,7 @@ export class RenderInfo extends BaseRenderInfo { ), ) - this.constants_.CORNER_RADIUS; return topHeight; - } else if (!bottomRow.hasNextConnection && this.hasStatementInput) { + } else if (!next.hasNextConnection && this.hasStatementInput) { return Math.abs( this.constants_.NOTCH_HEIGHT - this.constants_.CORNER_RADIUS, ); @@ -259,7 +256,7 @@ export class RenderInfo extends BaseRenderInfo { ) { return row.yPos + this.constants_.EMPTY_STATEMENT_INPUT_HEIGHT / 2; } - if (Types.isInlineInput(elem) && elem instanceof InputConnection) { + if (Types.isInlineInput(elem)) { const connectedBlock = elem.connectedBlock; if ( connectedBlock && @@ -308,7 +305,6 @@ export class RenderInfo extends BaseRenderInfo { } if ( Types.isField(elem) && - elem instanceof Field && elem.parentInput === this.rightAlignedDummyInputs.get(row) ) { break; @@ -371,7 +367,6 @@ export class RenderInfo extends BaseRenderInfo { xCursor < minXPos && !( Types.isField(elem) && - elem instanceof Field && (elem.field instanceof FieldLabel || elem.field instanceof FieldImage) ) @@ -525,7 +520,7 @@ export class RenderInfo extends BaseRenderInfo { return 0; } } - if (Types.isInlineInput(elem) && elem instanceof InputConnection) { + if (Types.isInlineInput(elem)) { const connectedBlock = elem.connectedBlock; const innerShape = connectedBlock ? (connectedBlock.pathObject as PathObject).outputShapeType @@ -552,7 +547,7 @@ export class RenderInfo extends BaseRenderInfo { connectionWidth - this.constants_.SHAPE_IN_SHAPE_PADDING[outerShape][innerShape] ); - } else if (Types.isField(elem) && elem instanceof Field) { + } else if (Types.isField(elem)) { // Special case for text inputs. if ( outerShape === constants.SHAPES.ROUND && @@ -616,7 +611,6 @@ export class RenderInfo extends BaseRenderInfo { for (let j = 0; j < row.elements.length; j++) { const elem = row.elements[j]; if ( - elem instanceof InputConnection && Types.isInlineInput(elem) && elem.connectedBlock && !elem.connectedBlock.isShadow() && From 4dcffa0914f3a4ce4a5c9ac164381262214497a6 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 8 Jan 2025 09:37:28 -0800 Subject: [PATCH 090/222] fix: Don't create intermediate variables when renaming a procedure argument. (#8723) --- blocks/procedures.ts | 104 ++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 56 deletions(-) diff --git a/blocks/procedures.ts b/blocks/procedures.ts index bac430c2260..7284973d9cc 100644 --- a/blocks/procedures.ts +++ b/blocks/procedures.ts @@ -629,30 +629,49 @@ type ArgumentBlock = Block & ArgumentMixin; interface ArgumentMixin extends ArgumentMixinType {} type ArgumentMixinType = typeof PROCEDURES_MUTATORARGUMENT; -// TODO(#6920): This is kludgy. -type FieldTextInputForArgument = FieldTextInput & { - oldShowEditorFn_(_e?: Event, quietInput?: boolean): void; - createdVariables_: IVariableModel[]; -}; +/** + * Field responsible for editing procedure argument names. + */ +class ProcedureArgumentField extends FieldTextInput { + /** + * Whether or not this field is currently being edited interactively. + */ + editingInteractively = false; + + /** + * The procedure argument variable whose name is being interactively edited. + */ + editingVariable?: IVariableModel; + + /** + * Displays the field editor. + * + * @param e The event that triggered display of the field editor. + */ + protected override showEditor_(e?: Event) { + super.showEditor_(e); + this.editingInteractively = true; + this.editingVariable = undefined; + } + + /** + * Handles cleanup when the field editor is dismissed. + */ + override onFinishEditing_(value: string) { + super.onFinishEditing_(value); + this.editingInteractively = false; + } +} const PROCEDURES_MUTATORARGUMENT = { /** * Mutator block for procedure argument. */ init: function (this: ArgumentBlock) { - const field = fieldRegistry.fromJson({ - type: 'field_input', - text: Procedures.DEFAULT_ARG, - }) as FieldTextInputForArgument; - field.setValidator(this.validator_); - // Hack: override showEditor to do just a little bit more work. - // We don't have a good place to hook into the start of a text edit. - field.oldShowEditorFn_ = (field as AnyDuringMigration).showEditor_; - const newShowEditorFn = function (this: typeof field) { - this.createdVariables_ = []; - this.oldShowEditorFn_(); - }; - (field as AnyDuringMigration).showEditor_ = newShowEditorFn; + const field = new ProcedureArgumentField( + Procedures.DEFAULT_ARG, + this.validator_, + ); this.appendDummyInput() .appendField(Msg['PROCEDURES_MUTATORARG_TITLE']) @@ -662,14 +681,6 @@ const PROCEDURES_MUTATORARGUMENT = { this.setStyle('procedure_blocks'); this.setTooltip(Msg['PROCEDURES_MUTATORARG_TOOLTIP']); this.contextMenu = false; - - // Create the default variable when we drag the block in from the flyout. - // Have to do this after installing the field on the block. - field.onFinishEditing_ = this.deleteIntermediateVars_; - // Create an empty list so onFinishEditing_ has something to look at, even - // though the editor was never opened. - field.createdVariables_ = []; - field.onFinishEditing_('x'); }, /** @@ -683,11 +694,11 @@ const PROCEDURES_MUTATORARGUMENT = { * @returns Valid name, or null if a name was not specified. */ validator_: function ( - this: FieldTextInputForArgument, + this: ProcedureArgumentField, varName: string, ): string | null { const sourceBlock = this.getSourceBlock()!; - const outerWs = sourceBlock!.workspace.getRootWorkspace()!; + const outerWs = sourceBlock.workspace.getRootWorkspace()!; varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); if (!varName) { return null; @@ -716,43 +727,24 @@ const PROCEDURES_MUTATORARGUMENT = { return varName; } - let model = outerWs.getVariable(varName, ''); + const model = outerWs.getVariable(varName, ''); if (model && model.getName() !== varName) { // Rename the variable (case change) outerWs.renameVariableById(model.getId(), varName); } if (!model) { - model = outerWs.createVariable(varName, ''); - if (model && this.createdVariables_) { - this.createdVariables_.push(model); + if (this.editingInteractively) { + if (!this.editingVariable) { + this.editingVariable = outerWs.createVariable(varName, ''); + } else { + outerWs.renameVariableById(this.editingVariable.getId(), varName); + } + } else { + outerWs.createVariable(varName, ''); } } return varName; }, - - /** - * Called when focusing away from the text field. - * Deletes all variables that were created as the user typed their intended - * variable name. - * - * @internal - * @param newText The new variable name. - */ - deleteIntermediateVars_: function ( - this: FieldTextInputForArgument, - newText: string, - ) { - const outerWs = this.getSourceBlock()!.workspace.getRootWorkspace(); - if (!outerWs) { - return; - } - for (let i = 0; i < this.createdVariables_.length; i++) { - const model = this.createdVariables_[i]; - if (model.getName() !== newText) { - outerWs.deleteVariableById(model.getId()); - } - } - }, }; blocks['procedures_mutatorarg'] = PROCEDURES_MUTATORARGUMENT; From 80a6d85c263ffc1912041315650a2a2ccd699163 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 8 Jan 2025 11:50:18 -0800 Subject: [PATCH 091/222] refactor!: Use JSON instead of XML for defining dynamic toolbox categories. (#8658) * refactor!: Use JSON instead of XML for defining dynamic toolbox categories. * chore: Fix tests. * chore: Remove unused import. * chore: Update docstrings. * chore: Revert removal of XML-based category functions. * chore: Add deprecation notices. --- core/procedures.ts | 107 +++++++++++++++++++++- core/variables.ts | 152 +++++++++++++++++++++++++++++++- core/variables_dynamic.ts | 91 ++++++++++++++++++- core/workspace_svg.ts | 12 +-- tests/mocha/contextmenu_test.js | 11 ++- tests/mocha/xml_test.js | 60 ------------- 6 files changed, 356 insertions(+), 77 deletions(-) diff --git a/core/procedures.ts b/core/procedures.ts index a16b0fce44a..73f06836cfe 100644 --- a/core/procedures.ts +++ b/core/procedures.ts @@ -42,6 +42,8 @@ import {IProcedureModel} from './interfaces/i_procedure_model.js'; import {Msg} from './msg.js'; import {Names} from './names.js'; import {ObservableProcedureMap} from './observable_procedure_map.js'; +import * as deprecation from './utils/deprecation.js'; +import type {FlyoutItemInfo} from './utils/toolbox.js'; import * as utilsXml from './utils/xml.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; @@ -238,7 +240,7 @@ export function rename(this: Field, name: string): string { * @param workspace The workspace containing procedures. * @returns Array of XML block elements. */ -export function flyoutCategory(workspace: WorkspaceSvg): Element[] { +function xmlFlyoutCategory(workspace: WorkspaceSvg): Element[] { const xmlList = []; if (Blocks['procedures_defnoreturn']) { // @@ -322,6 +324,109 @@ export function flyoutCategory(workspace: WorkspaceSvg): Element[] { return xmlList; } +/** + * Internal wrapper that returns the contents of the procedure category. + * + * @internal + * @param workspace The workspace to populate procedure blocks for. + */ +export function internalFlyoutCategory( + workspace: WorkspaceSvg, +): FlyoutItemInfo[] { + return flyoutCategory(workspace, false); +} + +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml: true, +): Element[]; +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml: false, +): FlyoutItemInfo[]; +/** + * Construct the blocks required by the flyout for the procedure category. + * + * @param workspace The workspace containing procedures. + * @param useXml True to return the contents as XML, false to use JSON. + * @returns List of flyout contents as either XML or JSON. + */ +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml = true, +): Element[] | FlyoutItemInfo[] { + if (useXml) { + deprecation.warn( + 'The XML return value of Blockly.Procedures.flyoutCategory()', + 'v12', + 'v13', + 'the same method, but handle a return type of FlyoutItemInfo[] (JSON) instead.', + ); + return xmlFlyoutCategory(workspace); + } + const blocks = []; + if (Blocks['procedures_defnoreturn']) { + blocks.push({ + 'kind': 'block', + 'type': 'procedures_defnoreturn', + 'gap': 16, + 'fields': { + 'NAME': Msg['PROCEDURES_DEFNORETURN_PROCEDURE'], + }, + }); + } + if (Blocks['procedures_defreturn']) { + blocks.push({ + 'kind': 'block', + 'type': 'procedures_defreturn', + 'gap': 16, + 'fields': { + 'NAME': Msg['PROCEDURES_DEFRETURN_PROCEDURE'], + }, + }); + } + if (Blocks['procedures_ifreturn']) { + blocks.push({ + 'kind': 'block', + 'type': 'procedures_ifreturn', + 'gap': 16, + }); + } + if (blocks.length) { + // Add slightly larger gap between system blocks and user calls. + blocks[blocks.length - 1]['gap'] = 24; + } + + /** + * Creates JSON block definitions for each of the given procedures. + * + * @param procedureList A list of procedures, each of which is defined by a + * three-element list of name, parameter list, and return value boolean. + * @param templateName The type of the block to generate. + */ + function populateProcedures( + procedureList: ProcedureTuple[], + templateName: string, + ) { + for (const [name, args] of procedureList) { + blocks.push({ + 'kind': 'block', + 'type': templateName, + 'gap': 16, + 'extraState': { + 'name': name, + 'params': args, + }, + }); + } + } + + const tuple = allProcedures(workspace); + populateProcedures(tuple[0], 'procedures_callnoreturn'); + populateProcedures(tuple[1], 'procedures_callreturn'); + return blocks; +} + /** * Updates the procedure mutator's flyout so that the arg block is not a * duplicate of another arg. diff --git a/core/variables.ts b/core/variables.ts index 5d60b475b1d..c896efd0f1a 100644 --- a/core/variables.ts +++ b/core/variables.ts @@ -13,6 +13,8 @@ import {isLegacyProcedureDefBlock} from './interfaces/i_legacy_procedure_blocks. import {isVariableBackedParameterModel} from './interfaces/i_variable_backed_parameter_model.js'; import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import {Msg} from './msg.js'; +import * as deprecation from './utils/deprecation.js'; +import type {BlockInfo, FlyoutItemInfo} from './utils/toolbox.js'; import * as utilsXml from './utils/xml.js'; import type {Workspace} from './workspace.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -84,19 +86,165 @@ export function allDeveloperVariables(workspace: Workspace): string[] { return Array.from(variables.values()); } +/** + * Internal wrapper that returns the contents of the variables category. + * + * @internal + * @param workspace The workspace to populate variable blocks for. + */ +export function internalFlyoutCategory( + workspace: WorkspaceSvg, +): FlyoutItemInfo[] { + return flyoutCategory(workspace, false); +} + +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml: true, +): Element[]; +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml: false, +): FlyoutItemInfo[]; /** * Construct the elements (blocks and button) required by the flyout for the * variable category. * * @param workspace The workspace containing variables. - * @returns Array of XML elements. + * @param useXml True to return the contents as XML, false to use JSON. + * @returns List of flyout contents as either XML or JSON. */ -export function flyoutCategory(workspace: WorkspaceSvg): Element[] { +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml = true, +): Element[] | FlyoutItemInfo[] { if (!Blocks['variables_set'] && !Blocks['variables_get']) { console.warn( 'There are no variable blocks, but there is a variable category.', ); } + + if (useXml) { + deprecation.warn( + 'The XML return value of Blockly.Variables.flyoutCategory()', + 'v12', + 'v13', + 'the same method, but handle a return type of FlyoutItemInfo[] (JSON) instead.', + ); + return xmlFlyoutCategory(workspace); + } + + workspace.registerButtonCallback('CREATE_VARIABLE', function (button) { + createVariableButtonHandler(button.getTargetWorkspace()); + }); + + return [ + { + 'kind': 'button', + 'text': '%{BKY_NEW_VARIABLE}', + 'callbackkey': 'CREATE_VARIABLE', + }, + ...jsonFlyoutCategoryBlocks( + workspace, + workspace.getVariablesOfType(''), + true, + ), + ]; +} + +/** + * Returns the JSON definition for a variable field. + * + * @param variable The variable the field should reference. + * @returns JSON for a variable field. + */ +function generateVariableFieldJson(variable: IVariableModel) { + return { + 'VAR': { + 'name': variable.getName(), + 'type': variable.getType(), + }, + }; +} + +/** + * Construct the blocks required by the flyout for the variable category. + * + * @internal + * @param workspace The workspace containing variables. + * @param variables List of variables to create blocks for. + * @param includeChangeBlocks True to include `change x by _` blocks. + * @param getterType The type of the variable getter block to generate. + * @param setterType The type of the variable setter block to generate. + * @returns JSON list of blocks. + */ +export function jsonFlyoutCategoryBlocks( + workspace: Workspace, + variables: IVariableModel[], + includeChangeBlocks: boolean, + getterType = 'variables_get', + setterType = 'variables_set', +): BlockInfo[] { + includeChangeBlocks &&= Blocks['math_change']; + + const blocks = []; + const mostRecentVariable = variables.slice(-1)[0]; + if (mostRecentVariable) { + // Show one setter block, with the name of the most recently created variable. + if (Blocks[setterType]) { + blocks.push({ + kind: 'block', + type: setterType, + gap: includeChangeBlocks ? 8 : 24, + fields: generateVariableFieldJson(mostRecentVariable), + }); + } + + if (includeChangeBlocks) { + blocks.push({ + 'kind': 'block', + 'type': 'math_change', + 'gap': Blocks[getterType] ? 20 : 8, + 'fields': generateVariableFieldJson(mostRecentVariable), + 'inputs': { + 'DELTA': { + 'shadow': { + 'type': 'math_number', + 'fields': { + 'NUM': 1, + }, + }, + }, + }, + }); + } + } + + if (Blocks[getterType]) { + // Show one getter block for each variable, sorted in alphabetical order. + blocks.push( + ...variables.sort(compareByName).map((variable) => { + return { + 'kind': 'block', + 'type': getterType, + 'gap': 8, + 'fields': generateVariableFieldJson(variable), + }; + }), + ); + } + + return blocks; +} + +/** + * Construct the elements (blocks and button) required by the flyout for the + * variable category. + * + * @param workspace The workspace containing variables. + * @returns Array of XML elements. + */ +function xmlFlyoutCategory(workspace: WorkspaceSvg): Element[] { let xmlList = new Array(); const button = document.createElement('button'); button.setAttribute('text', '%{BKY_NEW_VARIABLE}'); diff --git a/core/variables_dynamic.ts b/core/variables_dynamic.ts index 6722f8b49f8..4e2682ce8e9 100644 --- a/core/variables_dynamic.ts +++ b/core/variables_dynamic.ts @@ -9,6 +9,8 @@ import {Blocks} from './blocks.js'; import type {FlyoutButton} from './flyout_button.js'; import {Msg} from './msg.js'; +import * as deprecation from './utils/deprecation.js'; +import type {FlyoutItemInfo} from './utils/toolbox.js'; import * as xml from './utils/xml.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; @@ -68,19 +70,100 @@ function colourButtonClickHandler(button: FlyoutButton) { // eslint-disable-next-line camelcase export const onCreateVariableButtonClick_Colour = colourButtonClickHandler; +/** + * Internal wrapper that returns the contents of the dynamic variables category. + * + * @internal + * @param workspace The workspace to populate variable blocks for. + */ +export function internalFlyoutCategory( + workspace: WorkspaceSvg, +): FlyoutItemInfo[] { + return flyoutCategory(workspace, false); +} + +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml: true, +): Element[]; +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml: false, +): FlyoutItemInfo[]; /** * Construct the elements (blocks and button) required by the flyout for the - * variable category. + * dynamic variables category. * - * @param workspace The workspace containing variables. - * @returns Array of XML elements. + * @param useXml True to return the contents as XML, false to use JSON. + * @returns List of flyout contents as either XML or JSON. */ -export function flyoutCategory(workspace: WorkspaceSvg): Element[] { +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml = true, +): Element[] | FlyoutItemInfo[] { if (!Blocks['variables_set_dynamic'] && !Blocks['variables_get_dynamic']) { console.warn( 'There are no dynamic variable blocks, but there is a dynamic variable category.', ); } + + if (useXml) { + deprecation.warn( + 'The XML return value of Blockly.VariablesDynamic.flyoutCategory()', + 'v12', + 'v13', + 'the same method, but handle a return type of FlyoutItemInfo[] (JSON) instead.', + ); + return xmlFlyoutCategory(workspace); + } + + workspace.registerButtonCallback( + 'CREATE_VARIABLE_STRING', + stringButtonClickHandler, + ); + workspace.registerButtonCallback( + 'CREATE_VARIABLE_NUMBER', + numberButtonClickHandler, + ); + workspace.registerButtonCallback( + 'CREATE_VARIABLE_COLOUR', + colourButtonClickHandler, + ); + + return [ + { + 'kind': 'button', + 'text': Msg['NEW_STRING_VARIABLE'], + 'callbackkey': 'CREATE_VARIABLE_STRING', + }, + { + 'kind': 'button', + 'text': Msg['NEW_NUMBER_VARIABLE'], + 'callbackkey': 'CREATE_VARIABLE_NUMBER', + }, + { + 'kind': 'button', + 'text': Msg['NEW_COLOUR_VARIABLE'], + 'callbackkey': 'CREATE_VARIABLE_COLOUR', + }, + ...Variables.jsonFlyoutCategoryBlocks( + workspace, + workspace.getAllVariables(), + false, + 'variables_get_dynamic', + 'variables_set_dynamic', + ), + ]; +} + +/** + * Construct the elements (blocks and button) required by the flyout for the + * variable category. + * + * @param workspace The workspace containing variables. + * @returns Array of XML elements. + */ +function xmlFlyoutCategory(workspace: WorkspaceSvg): Element[] { let xmlList = new Array(); let button = document.createElement('button'); button.setAttribute('text', Msg['NEW_STRING_VARIABLE']); diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 60288573cec..e9aac5b9de1 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -365,24 +365,24 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** Manager in charge of markers and cursors. */ this.markerManager = new MarkerManager(this); - if (Variables && Variables.flyoutCategory) { + if (Variables && Variables.internalFlyoutCategory) { this.registerToolboxCategoryCallback( Variables.CATEGORY_NAME, - Variables.flyoutCategory, + Variables.internalFlyoutCategory, ); } - if (VariablesDynamic && VariablesDynamic.flyoutCategory) { + if (VariablesDynamic && VariablesDynamic.internalFlyoutCategory) { this.registerToolboxCategoryCallback( VariablesDynamic.CATEGORY_NAME, - VariablesDynamic.flyoutCategory, + VariablesDynamic.internalFlyoutCategory, ); } - if (Procedures && Procedures.flyoutCategory) { + if (Procedures && Procedures.internalFlyoutCategory) { this.registerToolboxCategoryCallback( Procedures.CATEGORY_NAME, - Procedures.flyoutCategory, + Procedures.internalFlyoutCategory, ); this.addChangeListener(Procedures.mutatorOpenListener); } diff --git a/tests/mocha/contextmenu_test.js b/tests/mocha/contextmenu_test.js index fe6d4be997e..65896112bb1 100644 --- a/tests/mocha/contextmenu_test.js +++ b/tests/mocha/contextmenu_test.js @@ -6,7 +6,6 @@ import {callbackFactory} from '../../build/src/core/contextmenu.js'; import * as xmlUtils from '../../build/src/core/utils/xml.js'; -import * as Variables from '../../build/src/core/variables.js'; import {assert} from '../../node_modules/chai/chai.js'; import { sharedTestSetup, @@ -32,9 +31,13 @@ suite('Context Menu', function () { }); test('callback with xml state creates block', function () { - const xmlField = Variables.generateVariableFieldDom( - this.forLoopBlock.getField('VAR').getVariable(), - ); + const variable = this.forLoopBlock.getField('VAR').getVariable(); + const xmlField = document.createElement('field'); + xmlField.setAttribute('name', 'VAR'); + xmlField.setAttribute('id', variable.getId()); + xmlField.setAttribute('variabletype', variable.getType()); + xmlField.textContent = variable.getName(); + const xmlBlock = xmlUtils.createElement('block'); xmlBlock.setAttribute('type', 'variables_get'); xmlBlock.appendChild(xmlField); diff --git a/tests/mocha/xml_test.js b/tests/mocha/xml_test.js index d30716edb44..218324197bf 100644 --- a/tests/mocha/xml_test.js +++ b/tests/mocha/xml_test.js @@ -531,28 +531,6 @@ suite('XML', function () { teardown(function () { workspaceTeardown.call(this, this.workspace); }); - suite('Dynamic Category Blocks', function () { - test('Untyped Variables', function () { - this.workspace.createVariable('name1', '', 'id1'); - const blocksArray = Blockly.Variables.flyoutCategoryBlocks( - this.workspace, - ); - for (let i = 0, xml; (xml = blocksArray[i]); i++) { - Blockly.Xml.domToBlock(xml, this.workspace); - } - }); - test('Typed Variables', function () { - this.workspace.createVariable('name1', 'String', 'id1'); - this.workspace.createVariable('name2', 'Number', 'id2'); - this.workspace.createVariable('name3', 'Colour', 'id3'); - const blocksArray = Blockly.VariablesDynamic.flyoutCategoryBlocks( - this.workspace, - ); - for (let i = 0, xml; (xml = blocksArray[i]); i++) { - Blockly.Xml.domToBlock(xml, this.workspace); - } - }); - }); suite('Comments', function () { suite('Headless', function () { test('Text', function () { @@ -910,42 +888,4 @@ suite('XML', function () { }); }); }); - suite('generateVariableFieldDom', function () { - test('Case Sensitive', function () { - const varId = 'testId'; - const type = 'testType'; - const name = 'testName'; - - const mockVariableModel = { - type: type, - name: name, - getId: function () { - return varId; - }, - getName: function () { - return name; - }, - getType: function () { - return type; - }, - }; - - const generatedXml = Blockly.Xml.domToText( - Blockly.Variables.generateVariableFieldDom(mockVariableModel), - ); - const expectedXml = - '' + - name + - ''; - assert.equal(generatedXml, expectedXml); - }); - }); }); From 75efba92e3338d66c786d15523dfd02494a40ebf Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 9 Jan 2025 14:31:51 -0800 Subject: [PATCH 092/222] fix: Fix bug that prevented keyboard navigation in flyouts. (#8687) * fix: Fix bug that prevented keyboard navigation in flyouts. * refactor: Add an `isFocusable()` method to FlyoutItem. --- core/block_flyout_inflater.ts | 30 +++++++++++++---- core/blockly.ts | 3 +- core/button_flyout_inflater.ts | 27 +++++++++++---- core/flyout_base.ts | 49 +++++++++++++--------------- core/flyout_horizontal.ts | 11 ++++--- core/flyout_item.ts | 42 ++++++++++++++++++++++++ core/flyout_vertical.ts | 15 +++++---- core/interfaces/i_flyout.ts | 2 +- core/interfaces/i_flyout_inflater.ts | 18 +++++++--- core/keyboard_nav/ast_node.ts | 27 ++++++++++----- core/label_flyout_inflater.ts | 31 ++++++++++++++---- core/separator_flyout_inflater.ts | 29 ++++++++++++---- 12 files changed, 203 insertions(+), 81 deletions(-) create mode 100644 core/flyout_item.ts diff --git a/core/block_flyout_inflater.ts b/core/block_flyout_inflater.ts index b888e5f3a81..0177ddf5067 100644 --- a/core/block_flyout_inflater.ts +++ b/core/block_flyout_inflater.ts @@ -10,7 +10,7 @@ import * as common from './common.js'; import {MANUALLY_DISABLED} from './constants.js'; import type {Abstract as AbstractEvent} from './events/events_abstract.js'; import {EventType} from './events/type.js'; -import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import {FlyoutItem} from './flyout_item.js'; import type {IFlyout} from './interfaces/i_flyout.js'; import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import * as registry from './registry.js'; @@ -27,6 +27,8 @@ import * as Xml from './xml.js'; const WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON = 'WORKSPACE_AT_BLOCK_CAPACITY'; +const BLOCK_TYPE = 'block'; + /** * Class responsible for creating blocks for flyouts. */ @@ -51,7 +53,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater { * @param flyoutWorkspace The workspace to create the block on. * @returns A newly created block. */ - load(state: object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { + load(state: object, flyoutWorkspace: WorkspaceSvg): FlyoutItem { this.setFlyoutWorkspace(flyoutWorkspace); this.flyout = flyoutWorkspace.targetWorkspace?.getFlyout() ?? undefined; const block = this.createBlock(state as BlockInfo, flyoutWorkspace); @@ -70,7 +72,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater { block.getDescendants(false).forEach((b) => (b.isInFlyout = true)); this.addBlockListeners(block); - return block; + return new FlyoutItem(block, BLOCK_TYPE, true); } /** @@ -114,7 +116,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater { * @param defaultGap The default spacing for flyout items. * @returns The amount of space that should follow this block. */ - gapForElement(state: object, defaultGap: number): number { + gapForItem(state: object, defaultGap: number): number { const blockState = state as BlockInfo; let gap; if (blockState['gap']) { @@ -134,9 +136,10 @@ export class BlockFlyoutInflater implements IFlyoutInflater { /** * Disposes of the given block. * - * @param element The flyout block to dispose of. + * @param item The flyout block to dispose of. */ - disposeElement(element: IBoundedElement): void { + disposeItem(item: FlyoutItem): void { + const element = item.getElement(); if (!(element instanceof BlockSvg)) return; this.removeListeners(element.id); element.dispose(false, false); @@ -257,6 +260,19 @@ export class BlockFlyoutInflater implements IFlyoutInflater { } }); } + + /** + * Returns the type of items this inflater is responsible for creating. + * + * @returns An identifier for the type of items this inflater creates. + */ + getType() { + return BLOCK_TYPE; + } } -registry.register(registry.Type.FLYOUT_INFLATER, 'block', BlockFlyoutInflater); +registry.register( + registry.Type.FLYOUT_INFLATER, + BLOCK_TYPE, + BlockFlyoutInflater, +); diff --git a/core/blockly.ts b/core/blockly.ts index d0543abbe15..a743ca5a7af 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -99,9 +99,10 @@ import { FieldVariableFromJsonConfig, FieldVariableValidator, } from './field_variable.js'; -import {Flyout, FlyoutItem} from './flyout_base.js'; +import {Flyout} from './flyout_base.js'; import {FlyoutButton} from './flyout_button.js'; import {HorizontalFlyout} from './flyout_horizontal.js'; +import {FlyoutItem} from './flyout_item.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import {FlyoutSeparator} from './flyout_separator.js'; import {VerticalFlyout} from './flyout_vertical.js'; diff --git a/core/button_flyout_inflater.ts b/core/button_flyout_inflater.ts index fc788ea5b90..2a9c3e289db 100644 --- a/core/button_flyout_inflater.ts +++ b/core/button_flyout_inflater.ts @@ -5,12 +5,14 @@ */ import {FlyoutButton} from './flyout_button.js'; -import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import {FlyoutItem} from './flyout_item.js'; import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import * as registry from './registry.js'; import {ButtonOrLabelInfo} from './utils/toolbox.js'; import type {WorkspaceSvg} from './workspace_svg.js'; +const BUTTON_TYPE = 'button'; + /** * Class responsible for creating buttons for flyouts. */ @@ -22,7 +24,7 @@ export class ButtonFlyoutInflater implements IFlyoutInflater { * @param flyoutWorkspace The workspace to create the button on. * @returns A newly created FlyoutButton. */ - load(state: object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { + load(state: object, flyoutWorkspace: WorkspaceSvg): FlyoutItem { const button = new FlyoutButton( flyoutWorkspace, flyoutWorkspace.targetWorkspace!, @@ -30,7 +32,8 @@ export class ButtonFlyoutInflater implements IFlyoutInflater { false, ); button.show(); - return button; + + return new FlyoutItem(button, BUTTON_TYPE, true); } /** @@ -40,24 +43,34 @@ export class ButtonFlyoutInflater implements IFlyoutInflater { * @param defaultGap The default spacing for flyout items. * @returns The amount of space that should follow this button. */ - gapForElement(state: object, defaultGap: number): number { + gapForItem(state: object, defaultGap: number): number { return defaultGap; } /** * Disposes of the given button. * - * @param element The flyout button to dispose of. + * @param item The flyout button to dispose of. */ - disposeElement(element: IBoundedElement): void { + disposeItem(item: FlyoutItem): void { + const element = item.getElement(); if (element instanceof FlyoutButton) { element.dispose(); } } + + /** + * Returns the type of items this inflater is responsible for creating. + * + * @returns An identifier for the type of items this inflater creates. + */ + getType() { + return BUTTON_TYPE; + } } registry.register( registry.Type.FLYOUT_INFLATER, - 'button', + BUTTON_TYPE, ButtonFlyoutInflater, ); diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 87aba011932..817076b97a1 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -18,16 +18,17 @@ import {DeleteArea} from './delete_area.js'; import type {Abstract as AbstractEvent} from './events/events_abstract.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; +import {FlyoutItem} from './flyout_item.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; import {IAutoHideable} from './interfaces/i_autohideable.js'; -import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IFlyout} from './interfaces/i_flyout.js'; import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import * as renderManagement from './render_management.js'; import {ScrollbarPair} from './scrollbar_pair.js'; +import {SEPARATOR_TYPE} from './separator_flyout_inflater.js'; import * as blocks from './serialization/blocks.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; @@ -677,20 +678,19 @@ export abstract class Flyout const type = info['kind'].toLowerCase(); const inflater = this.getInflaterForType(type); if (inflater) { - const element = inflater.load(info, this.getWorkspace()); - contents.push({ - type, - element, - }); - const gap = inflater.gapForElement(info, defaultGap); + contents.push(inflater.load(info, this.getWorkspace())); + const gap = inflater.gapForItem(info, defaultGap); if (gap) { - contents.push({ - type: 'sep', - element: new FlyoutSeparator( - gap, - this.horizontalLayout ? SeparatorAxis.X : SeparatorAxis.Y, + contents.push( + new FlyoutItem( + new FlyoutSeparator( + gap, + this.horizontalLayout ? SeparatorAxis.X : SeparatorAxis.Y, + ), + SEPARATOR_TYPE, + false, ), - }); + ); } } } @@ -711,9 +711,12 @@ export abstract class Flyout */ protected normalizeSeparators(contents: FlyoutItem[]): FlyoutItem[] { for (let i = contents.length - 1; i > 0; i--) { - const elementType = contents[i].type.toLowerCase(); - const previousElementType = contents[i - 1].type.toLowerCase(); - if (elementType === 'sep' && previousElementType === 'sep') { + const elementType = contents[i].getType().toLowerCase(); + const previousElementType = contents[i - 1].getType().toLowerCase(); + if ( + elementType === SEPARATOR_TYPE && + previousElementType === SEPARATOR_TYPE + ) { // Remove previousElement from the array, shifting the current element // forward as a result. This preserves the behavior where explicit // separator elements override the value of prior implicit (or explicit) @@ -752,9 +755,9 @@ export abstract class Flyout * Delete elements from a previous showing of the flyout. */ private clearOldBlocks() { - this.getContents().forEach((element) => { - const inflater = this.getInflaterForType(element.type); - inflater?.disposeElement(element.element); + this.getContents().forEach((item) => { + const inflater = this.getInflaterForType(item.getType()); + inflater?.disposeItem(item); }); // Clear potential variables from the previous showing. @@ -959,11 +962,3 @@ export abstract class Flyout return null; } } - -/** - * A flyout content item. - */ -export interface FlyoutItem { - type: string; - element: IBoundedElement; -} diff --git a/core/flyout_horizontal.ts b/core/flyout_horizontal.ts index d19320c8297..47b7ab06abd 100644 --- a/core/flyout_horizontal.ts +++ b/core/flyout_horizontal.ts @@ -13,7 +13,8 @@ import * as browserEvents from './browser_events.js'; import * as dropDownDiv from './dropdowndiv.js'; -import {Flyout, FlyoutItem} from './flyout_base.js'; +import {Flyout} from './flyout_base.js'; +import type {FlyoutItem} from './flyout_item.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import {Scrollbar} from './scrollbar.js'; @@ -263,10 +264,10 @@ export class HorizontalFlyout extends Flyout { } for (const item of contents) { - const rect = item.element.getBoundingRectangle(); + const rect = item.getElement().getBoundingRectangle(); const moveX = this.RTL ? cursorX + rect.getWidth() : cursorX; - item.element.moveBy(moveX, cursorY); - cursorX += item.element.getBoundingRectangle().getWidth(); + item.getElement().moveBy(moveX, cursorY); + cursorX += item.getElement().getBoundingRectangle().getWidth(); } } @@ -336,7 +337,7 @@ export class HorizontalFlyout extends Flyout { let flyoutHeight = this.getContents().reduce((maxHeightSoFar, item) => { return Math.max( maxHeightSoFar, - item.element.getBoundingRectangle().getHeight(), + item.getElement().getBoundingRectangle().getHeight(), ); }, 0); flyoutHeight += this.MARGIN * 1.5; diff --git a/core/flyout_item.ts b/core/flyout_item.ts new file mode 100644 index 00000000000..d501ceedbbf --- /dev/null +++ b/core/flyout_item.ts @@ -0,0 +1,42 @@ +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; + +/** + * Representation of an item displayed in a flyout. + */ +export class FlyoutItem { + /** + * Creates a new FlyoutItem. + * + * @param element The element that will be displayed in the flyout. + * @param type The type of element. Should correspond to the type of the + * flyout inflater that created this object. + * @param focusable True if the element should be allowed to be focused by + * e.g. keyboard navigation in the flyout. + */ + constructor( + private element: IBoundedElement, + private type: string, + private focusable: boolean, + ) {} + + /** + * Returns the element displayed in the flyout. + */ + getElement() { + return this.element; + } + + /** + * Returns the type of flyout element this item represents. + */ + getType() { + return this.type; + } + + /** + * Returns whether or not the flyout element can receive focus. + */ + isFocusable() { + return this.focusable; + } +} diff --git a/core/flyout_vertical.ts b/core/flyout_vertical.ts index 8e7c1691c1d..968b7c02458 100644 --- a/core/flyout_vertical.ts +++ b/core/flyout_vertical.ts @@ -13,7 +13,8 @@ import * as browserEvents from './browser_events.js'; import * as dropDownDiv from './dropdowndiv.js'; -import {Flyout, FlyoutItem} from './flyout_base.js'; +import {Flyout} from './flyout_base.js'; +import type {FlyoutItem} from './flyout_item.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import {Scrollbar} from './scrollbar.js'; @@ -229,8 +230,8 @@ export class VerticalFlyout extends Flyout { let cursorY = margin; for (const item of contents) { - item.element.moveBy(cursorX, cursorY); - cursorY += item.element.getBoundingRectangle().getHeight(); + item.getElement().moveBy(cursorX, cursorY); + cursorY += item.getElement().getBoundingRectangle().getHeight(); } } @@ -301,7 +302,7 @@ export class VerticalFlyout extends Flyout { let flyoutWidth = this.getContents().reduce((maxWidthSoFar, item) => { return Math.max( maxWidthSoFar, - item.element.getBoundingRectangle().getWidth(), + item.getElement().getBoundingRectangle().getWidth(), ); }, 0); flyoutWidth += this.MARGIN * 1.5 + this.tabWidth_; @@ -312,13 +313,13 @@ export class VerticalFlyout extends Flyout { if (this.RTL) { // With the flyoutWidth known, right-align the flyout contents. for (const item of this.getContents()) { - const oldX = item.element.getBoundingRectangle().left; + const oldX = item.getElement().getBoundingRectangle().left; const newX = flyoutWidth / this.workspace_.scale - - item.element.getBoundingRectangle().getWidth() - + item.getElement().getBoundingRectangle().getWidth() - this.MARGIN - this.tabWidth_; - item.element.moveBy(newX - oldX, 0); + item.getElement().moveBy(newX - oldX, 0); } } diff --git a/core/interfaces/i_flyout.ts b/core/interfaces/i_flyout.ts index c79be344c5a..42204775ece 100644 --- a/core/interfaces/i_flyout.ts +++ b/core/interfaces/i_flyout.ts @@ -7,7 +7,7 @@ // Former goog.module ID: Blockly.IFlyout import type {BlockSvg} from '../block_svg.js'; -import {FlyoutItem} from '../flyout_base.js'; +import type {FlyoutItem} from '../flyout_item.js'; import type {Coordinate} from '../utils/coordinate.js'; import type {Svg} from '../utils/svg.js'; import type {FlyoutDefinition} from '../utils/toolbox.js'; diff --git a/core/interfaces/i_flyout_inflater.ts b/core/interfaces/i_flyout_inflater.ts index 31f4c23fcbf..2deab770d25 100644 --- a/core/interfaces/i_flyout_inflater.ts +++ b/core/interfaces/i_flyout_inflater.ts @@ -1,5 +1,5 @@ +import type {FlyoutItem} from '../flyout_item.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; -import type {IBoundedElement} from './i_bounded_element.js'; export interface IFlyoutInflater { /** @@ -16,7 +16,7 @@ export interface IFlyoutInflater { * element, however. * @returns The newly inflated flyout element. */ - load(state: object, flyoutWorkspace: WorkspaceSvg): IBoundedElement; + load(state: object, flyoutWorkspace: WorkspaceSvg): FlyoutItem; /** * Returns the amount of spacing that should follow the element corresponding @@ -26,7 +26,7 @@ export interface IFlyoutInflater { * @param defaultGap The default gap for elements in this flyout. * @returns The gap that should follow the given element. */ - gapForElement(state: object, defaultGap: number): number; + gapForItem(state: object, defaultGap: number): number; /** * Disposes of the given element. @@ -37,5 +37,15 @@ export interface IFlyoutInflater { * * @param element The flyout element to dispose of. */ - disposeElement(element: IBoundedElement): void; + disposeItem(item: FlyoutItem): void; + + /** + * Returns the type of items that this inflater is responsible for inflating. + * This should be the same as the name under which this inflater registers + * itself, as well as the value returned by `getType()` on the `FlyoutItem` + * objects returned by `load()`. + * + * @returns The type of items this inflater creates. + */ + getType(): string; } diff --git a/core/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts index 1b3d72a01a5..ec46a4d3fd4 100644 --- a/core/keyboard_nav/ast_node.ts +++ b/core/keyboard_nav/ast_node.ts @@ -17,8 +17,8 @@ import {BlockSvg} from '../block_svg.js'; import type {Connection} from '../connection.js'; import {ConnectionType} from '../connection_type.js'; import type {Field} from '../field.js'; -import {FlyoutItem} from '../flyout_base.js'; import {FlyoutButton} from '../flyout_button.js'; +import type {FlyoutItem} from '../flyout_item.js'; import type {Input} from '../inputs/input.js'; import type {IASTNodeLocation} from '../interfaces/i_ast_node_location.js'; import type {IASTNodeLocationWithBlock} from '../interfaces/i_ast_node_location_with_block.js'; @@ -348,10 +348,11 @@ export class ASTNode { ); if (!nextItem) return null; - if (nextItem.element instanceof FlyoutButton) { - return ASTNode.createButtonNode(nextItem.element); - } else if (nextItem.element instanceof BlockSvg) { - return ASTNode.createStackNode(nextItem.element); + const element = nextItem.getElement(); + if (element instanceof FlyoutButton) { + return ASTNode.createButtonNode(element); + } else if (element instanceof BlockSvg) { + return ASTNode.createStackNode(element); } return null; @@ -373,13 +374,13 @@ export class ASTNode { const currentIndex = flyoutContents.findIndex((item: FlyoutItem) => { if ( currentLocation instanceof BlockSvg && - item.element === currentLocation + item.getElement() === currentLocation ) { return true; } if ( currentLocation instanceof FlyoutButton && - item.element === currentLocation + item.getElement() === currentLocation ) { return true; } @@ -388,7 +389,17 @@ export class ASTNode { if (currentIndex < 0) return null; - const resultIndex = forward ? currentIndex + 1 : currentIndex - 1; + let resultIndex = forward ? currentIndex + 1 : currentIndex - 1; + // The flyout may contain non-focusable elements like spacers or custom + // items. If the next/previous element is one of those, keep looking until a + // focusable element is encountered. + while ( + resultIndex >= 0 && + resultIndex < flyoutContents.length && + !flyoutContents[resultIndex].isFocusable() + ) { + resultIndex += forward ? 1 : -1; + } if (resultIndex === -1 || resultIndex === flyoutContents.length) { return null; } diff --git a/core/label_flyout_inflater.ts b/core/label_flyout_inflater.ts index ad304a9a634..51899ef23b4 100644 --- a/core/label_flyout_inflater.ts +++ b/core/label_flyout_inflater.ts @@ -5,12 +5,14 @@ */ import {FlyoutButton} from './flyout_button.js'; -import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import {FlyoutItem} from './flyout_item.js'; import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import * as registry from './registry.js'; import {ButtonOrLabelInfo} from './utils/toolbox.js'; import type {WorkspaceSvg} from './workspace_svg.js'; +const LABEL_TYPE = 'label'; + /** * Class responsible for creating labels for flyouts. */ @@ -22,7 +24,7 @@ export class LabelFlyoutInflater implements IFlyoutInflater { * @param flyoutWorkspace The workspace to create the label on. * @returns A FlyoutButton configured as a label. */ - load(state: object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { + load(state: object, flyoutWorkspace: WorkspaceSvg): FlyoutItem { const label = new FlyoutButton( flyoutWorkspace, flyoutWorkspace.targetWorkspace!, @@ -30,7 +32,8 @@ export class LabelFlyoutInflater implements IFlyoutInflater { true, ); label.show(); - return label; + + return new FlyoutItem(label, LABEL_TYPE, true); } /** @@ -40,20 +43,34 @@ export class LabelFlyoutInflater implements IFlyoutInflater { * @param defaultGap The default spacing for flyout items. * @returns The amount of space that should follow this label. */ - gapForElement(state: object, defaultGap: number): number { + gapForItem(state: object, defaultGap: number): number { return defaultGap; } /** * Disposes of the given label. * - * @param element The flyout label to dispose of. + * @param item The flyout label to dispose of. */ - disposeElement(element: IBoundedElement): void { + disposeItem(item: FlyoutItem): void { + const element = item.getElement(); if (element instanceof FlyoutButton) { element.dispose(); } } + + /** + * Returns the type of items this inflater is responsible for creating. + * + * @returns An identifier for the type of items this inflater creates. + */ + getType() { + return LABEL_TYPE; + } } -registry.register(registry.Type.FLYOUT_INFLATER, 'label', LabelFlyoutInflater); +registry.register( + registry.Type.FLYOUT_INFLATER, + LABEL_TYPE, + LabelFlyoutInflater, +); diff --git a/core/separator_flyout_inflater.ts b/core/separator_flyout_inflater.ts index 8c0acf2f5c5..0b9aa602615 100644 --- a/core/separator_flyout_inflater.ts +++ b/core/separator_flyout_inflater.ts @@ -4,13 +4,18 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {FlyoutItem} from './flyout_item.js'; import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; -import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import * as registry from './registry.js'; import type {SeparatorInfo} from './utils/toolbox.js'; import type {WorkspaceSvg} from './workspace_svg.js'; +/** + * @internal + */ +export const SEPARATOR_TYPE = 'sep'; + /** * Class responsible for creating separators for flyouts. */ @@ -33,12 +38,13 @@ export class SeparatorFlyoutInflater implements IFlyoutInflater { * @param flyoutWorkspace The workspace the separator belongs to. * @returns A newly created FlyoutSeparator. */ - load(_state: object, flyoutWorkspace: WorkspaceSvg): IBoundedElement { + load(_state: object, flyoutWorkspace: WorkspaceSvg): FlyoutItem { const flyoutAxis = flyoutWorkspace.targetWorkspace?.getFlyout() ?.horizontalLayout ? SeparatorAxis.X : SeparatorAxis.Y; - return new FlyoutSeparator(0, flyoutAxis); + const separator = new FlyoutSeparator(0, flyoutAxis); + return new FlyoutItem(separator, SEPARATOR_TYPE, false); } /** @@ -48,7 +54,7 @@ export class SeparatorFlyoutInflater implements IFlyoutInflater { * @param defaultGap The default spacing for flyout items. * @returns The desired size of the separator. */ - gapForElement(state: object, defaultGap: number): number { + gapForItem(state: object, defaultGap: number): number { const separatorState = state as SeparatorInfo; const newGap = parseInt(String(separatorState['gap'])); return newGap ?? defaultGap; @@ -57,13 +63,22 @@ export class SeparatorFlyoutInflater implements IFlyoutInflater { /** * Disposes of the given separator. Intentional no-op. * - * @param _element The flyout separator to dispose of. + * @param _item The flyout separator to dispose of. + */ + disposeItem(_item: FlyoutItem): void {} + + /** + * Returns the type of items this inflater is responsible for creating. + * + * @returns An identifier for the type of items this inflater creates. */ - disposeElement(_element: IBoundedElement): void {} + getType() { + return SEPARATOR_TYPE; + } } registry.register( registry.Type.FLYOUT_INFLATER, - 'sep', + SEPARATOR_TYPE, SeparatorFlyoutInflater, ); From c68d6451b736827eb0f0efa425427a488cd576a8 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 10 Jan 2025 10:53:31 -0800 Subject: [PATCH 093/222] release: Update version number to 12.0.0-beta.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59102d98229..bd1267161ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockly", - "version": "12.0.0-beta.0", + "version": "12.0.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "12.0.0-beta.0", + "version": "12.0.0-beta.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 8e10e21638b..936fef256ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "12.0.0-beta.0", + "version": "12.0.0-beta.1", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly" From c88ebf1ede7ca188a08ef7a19d4190b41fe1e7f9 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 16 Jan 2025 15:23:55 -0800 Subject: [PATCH 094/222] fix: Don't add padding around zero-width fields. (#8738) --- core/field.ts | 20 -------------------- core/renderers/common/info.ts | 5 +++++ core/renderers/geras/info.ts | 6 ++++++ core/renderers/thrasos/info.ts | 6 ++++++ core/renderers/zelos/info.ts | 6 ++++++ 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/core/field.ts b/core/field.ts index 627cf4ef2f4..deae29af90b 100644 --- a/core/field.ts +++ b/core/field.ts @@ -83,9 +83,6 @@ export abstract class Field */ DEFAULT_VALUE: T | null = null; - /** Non-breaking space. */ - static readonly NBSP = '\u00A0'; - /** * A value used to signal when a field's constructor should *not* set the * field's value or run configure_, and should allow a subclass to do that @@ -905,17 +902,6 @@ export abstract class Field if (this.isDirty_) { this.render_(); this.isDirty_ = false; - } else if (this.visible_ && this.size_.width === 0) { - // If the field is not visible the width will be 0 as well, one of the - // problems with the old system. - this.render_(); - // Don't issue a warning if the field is actually zero width. - if (this.size_.width !== 0) { - console.warn( - 'Deprecated use of setting size_.width to 0 to rerender a' + - ' field. Set field.isDirty_ to true instead.', - ); - } } return this.size_; } @@ -979,16 +965,10 @@ export abstract class Field */ protected getDisplayText_(): string { let text = this.getText(); - if (!text) { - // Prevent the field from disappearing if empty. - return Field.NBSP; - } if (text.length > this.maxDisplayLength) { // Truncate displayed string and add an ellipsis ('...'). text = text.substring(0, this.maxDisplayLength - 2) + '…'; } - // Replace whitespace with non-breaking spaces so the text doesn't collapse. - text = text.replace(/\s/g, Field.NBSP); if (this.sourceBlock_ && this.sourceBlock_.RTL) { // The SVG is LTR, force text to be RTL by adding an RLM. text += '\u200F'; diff --git a/core/renderers/common/info.ts b/core/renderers/common/info.ts index f826d17e463..0e4d3e9460c 100644 --- a/core/renderers/common/info.ts +++ b/core/renderers/common/info.ts @@ -457,6 +457,11 @@ export class RenderInfo { } } + // Don't add padding after zero-width fields. + if (prev && Types.isField(prev) && prev.width === 0) { + return this.constants_.NO_PADDING; + } + return this.constants_.MEDIUM_PADDING; } diff --git a/core/renderers/geras/info.ts b/core/renderers/geras/info.ts index 3e1980aa0a5..11f9e764ac6 100644 --- a/core/renderers/geras/info.ts +++ b/core/renderers/geras/info.ts @@ -164,6 +164,9 @@ export class RenderInfo extends BaseRenderInfo { if (!Types.isInput(prev) && (!next || Types.isStatementInput(next))) { // Between an editable field and the end of the row. if (Types.isField(prev) && prev.isEditable) { + if (prev.width === 0) { + return this.constants_.NO_PADDING; + } return this.constants_.MEDIUM_PADDING; } // Padding at the end of an icon-only row to make the block shape clearer. @@ -276,6 +279,9 @@ export class RenderInfo extends BaseRenderInfo { Types.isField(next) && prev.isEditable === next.isEditable ) { + if (prev.width === 0) { + return this.constants_.NO_PADDING; + } return this.constants_.LARGE_PADDING; } diff --git a/core/renderers/thrasos/info.ts b/core/renderers/thrasos/info.ts index 3c8c19f9478..62c08fa424d 100644 --- a/core/renderers/thrasos/info.ts +++ b/core/renderers/thrasos/info.ts @@ -109,6 +109,9 @@ export class RenderInfo extends BaseRenderInfo { if (!Types.isInput(prev) && !next) { // Between an editable field and the end of the row. if (Types.isField(prev) && prev.isEditable) { + if (prev.width === 0) { + return this.constants_.NO_PADDING; + } return this.constants_.MEDIUM_PADDING; } // Padding at the end of an icon-only row to make the block shape clearer. @@ -204,6 +207,9 @@ export class RenderInfo extends BaseRenderInfo { Types.isField(next) && prev.isEditable === next.isEditable ) { + if (prev.width === 0) { + return this.constants_.NO_PADDING; + } return this.constants_.LARGE_PADDING; } diff --git a/core/renderers/zelos/info.ts b/core/renderers/zelos/info.ts index 5c507c33a79..e14c584f0dc 100644 --- a/core/renderers/zelos/info.ts +++ b/core/renderers/zelos/info.ts @@ -186,6 +186,12 @@ export class RenderInfo extends BaseRenderInfo { if (prev && Types.isLeftSquareCorner(prev) && next && Types.isHat(next)) { return this.constants_.NO_PADDING; } + + // No space after zero-width fields. + if (prev && Types.isField(prev) && prev.width === 0) { + return this.constants_.NO_PADDING; + } + return this.constants_.MEDIUM_PADDING; } From 343c2f51f3e34956f7e8efdc0fb220fe441e18d4 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 30 Jan 2025 13:47:36 -0800 Subject: [PATCH 095/222] 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 f5683fcca46..b95427bce4e 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 1f30852c513..a33a21a8505 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 f21ece8a1c7..190efd64dd1 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 c9a1ea0abf7..07b9bca5b0b 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 dd8b10fc2f9..9e051d5bc7c 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 817076b97a1..5b2e91b7c7a 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 0b65299e578..fc23ba7ca15 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 0db28a51a4b..0793e6213b4 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 265420ec050..30238b91e7f 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 e9aac5b9de1..78506115e88 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 0b72a7fee6b..82293f22440 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); }); From e6e57ddc01ae39f044a5aa7cc568be6d7206f523 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 4 Feb 2025 15:23:03 -0800 Subject: [PATCH 096/222] fix: Fix bug that caused blocks dragged from non-primary flyouts to be misplaced. (#8753) * fix: Fix bug that caused blocks dragged from non-primary flyouts to be misplaced. * chore: Fix docstring. --- core/block_flyout_inflater.ts | 58 +++++++++++++--------------- core/button_flyout_inflater.ts | 10 ++--- core/flyout_base.ts | 2 +- core/interfaces/i_flyout_inflater.ts | 6 +-- core/label_flyout_inflater.ts | 11 +++--- core/separator_flyout_inflater.ts | 9 ++--- 6 files changed, 45 insertions(+), 51 deletions(-) diff --git a/core/block_flyout_inflater.ts b/core/block_flyout_inflater.ts index 0177ddf5067..6011b150eba 100644 --- a/core/block_flyout_inflater.ts +++ b/core/block_flyout_inflater.ts @@ -35,7 +35,6 @@ const BLOCK_TYPE = 'block'; export class BlockFlyoutInflater implements IFlyoutInflater { protected permanentlyDisabledBlocks = new Set(); protected listeners = new Map(); - protected flyoutWorkspace?: WorkspaceSvg; protected flyout?: IFlyout; private capacityWrapper: (event: AbstractEvent) => void; @@ -50,13 +49,12 @@ export class BlockFlyoutInflater implements IFlyoutInflater { * Inflates a flyout block from the given state and adds it to the flyout. * * @param state A JSON representation of a flyout block. - * @param flyoutWorkspace The workspace to create the block on. + * @param flyout The flyout to create the block on. * @returns A newly created block. */ - load(state: object, flyoutWorkspace: WorkspaceSvg): FlyoutItem { - this.setFlyoutWorkspace(flyoutWorkspace); - this.flyout = flyoutWorkspace.targetWorkspace?.getFlyout() ?? undefined; - const block = this.createBlock(state as BlockInfo, flyoutWorkspace); + load(state: object, flyout: IFlyout): FlyoutItem { + this.setFlyout(flyout); + const block = this.createBlock(state as BlockInfo, flyout.getWorkspace()); if (!block.isEnabled()) { // Record blocks that were initially disabled. @@ -157,22 +155,18 @@ export class BlockFlyoutInflater implements IFlyoutInflater { } /** - * Updates this inflater's flyout workspace. + * Updates this inflater's flyout. * - * @param workspace The workspace of the flyout that owns this inflater. + * @param flyout The flyout that owns this inflater. */ - protected setFlyoutWorkspace(workspace: WorkspaceSvg) { - if (this.flyoutWorkspace === workspace) return; + protected setFlyout(flyout: IFlyout) { + if (this.flyout === flyout) return; - if (this.flyoutWorkspace) { - this.flyoutWorkspace.targetWorkspace?.removeChangeListener( - this.capacityWrapper, - ); + if (this.flyout) { + this.flyout.targetWorkspace?.removeChangeListener(this.capacityWrapper); } - this.flyoutWorkspace = workspace; - this.flyoutWorkspace.targetWorkspace?.addChangeListener( - this.capacityWrapper, - ); + this.flyout = flyout; + this.flyout.targetWorkspace?.addChangeListener(this.capacityWrapper); } /** @@ -182,7 +176,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater { * @param block The block to update the enabled/disabled state of. */ private updateStateBasedOnCapacity(block: BlockSvg) { - const enable = this.flyoutWorkspace?.targetWorkspace?.isCapacityAvailable( + const enable = this.flyout?.targetWorkspace?.isCapacityAvailable( common.getBlockTypeCounts(block), ); let currentBlock: BlockSvg | null = block; @@ -209,11 +203,10 @@ export class BlockFlyoutInflater implements IFlyoutInflater { 'pointerdown', block, (e: PointerEvent) => { - const gesture = this.flyoutWorkspace?.targetWorkspace?.getGesture(e); - const flyout = this.flyoutWorkspace?.targetWorkspace?.getFlyout(); - if (gesture && flyout) { + const gesture = this.flyout?.targetWorkspace?.getGesture(e); + if (gesture && this.flyout) { gesture.setStartBlock(block); - gesture.handleFlyoutStart(e, flyout); + gesture.handleFlyoutStart(e, this.flyout); } }, ), @@ -221,14 +214,14 @@ export class BlockFlyoutInflater implements IFlyoutInflater { blockListeners.push( browserEvents.bind(block.getSvgRoot(), 'pointerenter', null, () => { - if (!this.flyoutWorkspace?.targetWorkspace?.isDragging()) { + if (!this.flyout?.targetWorkspace?.isDragging()) { block.addSelect(); } }), ); blockListeners.push( browserEvents.bind(block.getSvgRoot(), 'pointerleave', null, () => { - if (!this.flyoutWorkspace?.targetWorkspace?.isDragging()) { + if (!this.flyout?.targetWorkspace?.isDragging()) { block.removeSelect(); } }), @@ -245,7 +238,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater { */ private filterFlyoutBasedOnCapacity(event: AbstractEvent) { if ( - !this.flyoutWorkspace || + !this.flyout || (event && !( event.type === EventType.BLOCK_CREATE || @@ -254,11 +247,14 @@ export class BlockFlyoutInflater implements IFlyoutInflater { ) return; - this.flyoutWorkspace.getTopBlocks(false).forEach((block) => { - if (!this.permanentlyDisabledBlocks.has(block)) { - this.updateStateBasedOnCapacity(block); - } - }); + this.flyout + .getWorkspace() + .getTopBlocks(false) + .forEach((block) => { + if (!this.permanentlyDisabledBlocks.has(block)) { + this.updateStateBasedOnCapacity(block); + } + }); } /** diff --git a/core/button_flyout_inflater.ts b/core/button_flyout_inflater.ts index 2a9c3e289db..665ce7a2425 100644 --- a/core/button_flyout_inflater.ts +++ b/core/button_flyout_inflater.ts @@ -6,10 +6,10 @@ import {FlyoutButton} from './flyout_button.js'; import {FlyoutItem} from './flyout_item.js'; +import type {IFlyout} from './interfaces/i_flyout.js'; import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import * as registry from './registry.js'; import {ButtonOrLabelInfo} from './utils/toolbox.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; const BUTTON_TYPE = 'button'; @@ -21,13 +21,13 @@ export class ButtonFlyoutInflater implements IFlyoutInflater { * Inflates a flyout button from the given state and adds it to the flyout. * * @param state A JSON representation of a flyout button. - * @param flyoutWorkspace The workspace to create the button on. + * @param flyout The flyout to create the button on. * @returns A newly created FlyoutButton. */ - load(state: object, flyoutWorkspace: WorkspaceSvg): FlyoutItem { + load(state: object, flyout: IFlyout): FlyoutItem { const button = new FlyoutButton( - flyoutWorkspace, - flyoutWorkspace.targetWorkspace!, + flyout.getWorkspace(), + flyout.targetWorkspace!, state as ButtonOrLabelInfo, false, ); diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 5b2e91b7c7a..e738470a606 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -678,7 +678,7 @@ export abstract class Flyout const type = info['kind'].toLowerCase(); const inflater = this.getInflaterForType(type); if (inflater) { - contents.push(inflater.load(info, this.getWorkspace())); + contents.push(inflater.load(info, this)); const gap = inflater.gapForItem(info, defaultGap); if (gap) { contents.push( diff --git a/core/interfaces/i_flyout_inflater.ts b/core/interfaces/i_flyout_inflater.ts index 2deab770d25..e3c1f5db48f 100644 --- a/core/interfaces/i_flyout_inflater.ts +++ b/core/interfaces/i_flyout_inflater.ts @@ -1,5 +1,5 @@ import type {FlyoutItem} from '../flyout_item.js'; -import type {WorkspaceSvg} from '../workspace_svg.js'; +import type {IFlyout} from './i_flyout.js'; export interface IFlyoutInflater { /** @@ -9,14 +9,14 @@ export interface IFlyoutInflater { * allow for code reuse. * * @param state A JSON representation of an element to inflate on the flyout. - * @param flyoutWorkspace The flyout's workspace, where the inflated element + * @param flyout The flyout on whose workspace the inflated element * should be created. If the inflated element is an `IRenderedElement` it * itself or the inflater should append it to the workspace; the flyout * will not do so itself. The flyout is responsible for positioning the * element, however. * @returns The newly inflated flyout element. */ - load(state: object, flyoutWorkspace: WorkspaceSvg): FlyoutItem; + load(state: object, flyout: IFlyout): FlyoutItem; /** * Returns the amount of spacing that should follow the element corresponding diff --git a/core/label_flyout_inflater.ts b/core/label_flyout_inflater.ts index 51899ef23b4..e4f3e3b54db 100644 --- a/core/label_flyout_inflater.ts +++ b/core/label_flyout_inflater.ts @@ -6,11 +6,10 @@ import {FlyoutButton} from './flyout_button.js'; import {FlyoutItem} from './flyout_item.js'; +import type {IFlyout} from './interfaces/i_flyout.js'; import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import * as registry from './registry.js'; import {ButtonOrLabelInfo} from './utils/toolbox.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; - const LABEL_TYPE = 'label'; /** @@ -21,13 +20,13 @@ export class LabelFlyoutInflater implements IFlyoutInflater { * Inflates a flyout label from the given state and adds it to the flyout. * * @param state A JSON representation of a flyout label. - * @param flyoutWorkspace The workspace to create the label on. + * @param flyout The flyout to create the label on. * @returns A FlyoutButton configured as a label. */ - load(state: object, flyoutWorkspace: WorkspaceSvg): FlyoutItem { + load(state: object, flyout: IFlyout): FlyoutItem { const label = new FlyoutButton( - flyoutWorkspace, - flyoutWorkspace.targetWorkspace!, + flyout.getWorkspace(), + flyout.targetWorkspace!, state as ButtonOrLabelInfo, true, ); diff --git a/core/separator_flyout_inflater.ts b/core/separator_flyout_inflater.ts index 0b9aa602615..63e53355478 100644 --- a/core/separator_flyout_inflater.ts +++ b/core/separator_flyout_inflater.ts @@ -6,10 +6,10 @@ import {FlyoutItem} from './flyout_item.js'; import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; +import type {IFlyout} from './interfaces/i_flyout.js'; import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import * as registry from './registry.js'; import type {SeparatorInfo} from './utils/toolbox.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; /** * @internal @@ -35,12 +35,11 @@ export class SeparatorFlyoutInflater implements IFlyoutInflater { * returned by gapForElement, which knows the default gap, unlike this method. * * @param _state A JSON representation of a flyout separator. - * @param flyoutWorkspace The workspace the separator belongs to. + * @param flyout The flyout to create the separator for. * @returns A newly created FlyoutSeparator. */ - load(_state: object, flyoutWorkspace: WorkspaceSvg): FlyoutItem { - const flyoutAxis = flyoutWorkspace.targetWorkspace?.getFlyout() - ?.horizontalLayout + load(_state: object, flyout: IFlyout): FlyoutItem { + const flyoutAxis = flyout.horizontalLayout ? SeparatorAxis.X : SeparatorAxis.Y; const separator = new FlyoutSeparator(0, flyoutAxis); From 3ae422a56657c728707b25842022c57e86f8ab3a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 14 Feb 2025 01:26:34 +0000 Subject: [PATCH 097/222] feat: Add interfaces for focus management. Introduces the necessary base interfaces for representing different focusable contexts within Blockly. The actual logic for utilizing and implementing these interfaces will come in later PRs. --- core/blockly.ts | 4 +++ core/interfaces/i_focusable_node.ts | 36 ++++++++++++++++++++ core/interfaces/i_focusable_tree.ts | 53 +++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 core/interfaces/i_focusable_node.ts create mode 100644 core/interfaces/i_focusable_tree.ts diff --git a/core/blockly.ts b/core/blockly.ts index a743ca5a7af..cf77bca3fb8 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -140,6 +140,8 @@ import { } from './interfaces/i_draggable.js'; import {IDragger} from './interfaces/i_dragger.js'; import {IFlyout} from './interfaces/i_flyout.js'; +import {IFocusableNode} from './interfaces/i_focusable_node.js'; +import {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {IHasBubble, hasBubble} from './interfaces/i_has_bubble.js'; import {IIcon, isIcon} from './interfaces/i_icon.js'; import {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js'; @@ -544,6 +546,8 @@ export { IDragger, IFlyout, IFlyoutInflater, + IFocusableNode, + IFocusableTree, IHasBubble, IIcon, IKeyboardAccessible, diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts new file mode 100644 index 00000000000..87a0293ae0c --- /dev/null +++ b/core/interfaces/i_focusable_node.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableTree} from './i_focusable_tree.js'; + +/** Represents anything that can have input focus. */ +export interface IFocusableNode { + /** + * Returns the DOM element that can be explicitly requested to receive focus. + * + * IMPORTANT: Please note that this element is expected to have a visual + * presence on the page as it will both be explicitly focused and have its + * style changed depending on its current focus state (i.e. blurred, actively + * focused, and passively focused). The element will have one of two styles + * attached (where no style indicates blurred/not focused): + * - blocklyActiveFocus + * - blocklyPassiveFocus + * + * The returned element must also have a valid ID specified, and unique to the + * element relative to its nearest IFocusableTree parent. + * + * It's expected the return element will not change for the lifetime of the + * node. + */ + getFocusableElement(): HTMLElement | SVGElement; + + /** + * Returns the closest parent tree of this node (in cases where a tree has + * distinct trees underneath it), which represents the tree to which this node + * belongs. + */ + getFocusableTree(): IFocusableTree; +} diff --git a/core/interfaces/i_focusable_tree.ts b/core/interfaces/i_focusable_tree.ts new file mode 100644 index 00000000000..21f87678d01 --- /dev/null +++ b/core/interfaces/i_focusable_tree.ts @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableNode} from './i_focusable_node.js'; + +/** + * Represents a tree of focusable elements with its own active/passive focus + * context. + * + * Note that focus is handled by FocusManager, and tree implementations can have + * at most one IFocusableNode focused at one time. If the tree itself has focus, + * then the tree's focused node is considered 'active' ('passive' if another + * tree has focus). + * + * Focus is shared between one or more trees, where each tree can have exactly + * one active or passive node (and only one active node can exist on the whole + * page at any given time). The idea of passive focus is to provide context to + * users on where their focus will be restored upon navigating back to a + * previously focused tree. + */ +export interface IFocusableTree { + /** + * Returns the current node with focus in this tree, or null if none (or if + * the root has focus). + * + * Note that this will never return a node from a nested sub-tree as that tree + * should specifically be called in order to retrieve its focused node. + */ + getFocusedNode(): IFocusableNode | null; + + /** + * Returns the top-level focusable node of the tree. + * + * It's expected that the returned node will be focused in cases where + * FocusManager wants to focus a tree in a situation where it does not + * currently have a focused node. + */ + getRootFocusableNode(): IFocusableNode; + + /** + * Returns the IFocusableNode corresponding to the select element, or null if + * the element does not have such a node. + * + * The provided element must have a non-null ID that conforms to the contract + * mentioned in IFocusableNode. + */ + findFocusableNodeFor( + element: HTMLElement | SVGElement, + ): IFocusableNode | null; +} From de076a7cf5193d3e064755298565c7358d0cbc18 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 19 Feb 2025 23:51:08 +0000 Subject: [PATCH 098/222] Empty commit to re-trigger CI flows. They didn't trigger automatically after a force push. From b343a13bbecd7db3b4359bd04abdfb5857318142 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Thu, 20 Feb 2025 08:56:57 -0800 Subject: [PATCH 099/222] fix: Fixes #8764 by moving the event grouping calls up to dragger.ts (#8781) --- core/dragging/block_drag_strategy.ts | 16 ++++++---------- core/dragging/bubble_drag_strategy.ts | 11 ----------- core/dragging/comment_drag_strategy.ts | 10 ---------- core/dragging/dragger.ts | 11 +++++++---- 4 files changed, 13 insertions(+), 35 deletions(-) diff --git a/core/dragging/block_drag_strategy.ts b/core/dragging/block_drag_strategy.ts index 07b9bca5b0b..9a2cd747cd4 100644 --- a/core/dragging/block_drag_strategy.ts +++ b/core/dragging/block_drag_strategy.ts @@ -62,8 +62,8 @@ export class BlockDragStrategy implements IDragStrategy { */ private dragOffset = new Coordinate(0, 0); - /** Was there already an event group in progress when the drag started? */ - private inGroup: boolean = false; + /** Used to persist an event group when snapping is done async. */ + private originalEventGroup = ''; constructor(private block: BlockSvg) { this.workspace = block.workspace; @@ -96,10 +96,6 @@ export class BlockDragStrategy implements IDragStrategy { } this.dragging = true; - this.inGroup = !!eventUtils.getGroup(); - if (!this.inGroup) { - eventUtils.setGroup(true); - } this.fireDragStartEvent(); this.startLoc = this.block.getRelativeToSurfaceXY(); @@ -363,6 +359,7 @@ export class BlockDragStrategy implements IDragStrategy { this.block.getParent()?.endDrag(e); return; } + this.originalEventGroup = eventUtils.getGroup(); this.fireDragEndEvent(); this.fireMoveEvent(); @@ -388,20 +385,19 @@ export class BlockDragStrategy implements IDragStrategy { } else { this.block.queueRender().then(() => this.disposeStep()); } - - if (!this.inGroup) { - eventUtils.setGroup(false); - } } /** Disposes of any state at the end of the drag. */ private disposeStep() { + const newGroup = eventUtils.getGroup(); + eventUtils.setGroup(this.originalEventGroup); this.block.snapToGrid(); // Must dispose after connections are applied to not break the dynamic // connections plugin. See #7859 this.connectionPreviewer!.dispose(); this.workspace.setResizesEnabled(true); + eventUtils.setGroup(newGroup); } /** Connects the given candidate connections. */ diff --git a/core/dragging/bubble_drag_strategy.ts b/core/dragging/bubble_drag_strategy.ts index c2a5c58f4a2..8a5a6783910 100644 --- a/core/dragging/bubble_drag_strategy.ts +++ b/core/dragging/bubble_drag_strategy.ts @@ -5,7 +5,6 @@ */ import {IBubble, WorkspaceSvg} from '../blockly.js'; -import * as eventUtils from '../events/utils.js'; import {IDragStrategy} from '../interfaces/i_draggable.js'; import * as layers from '../layers.js'; import {Coordinate} from '../utils.js'; @@ -13,9 +12,6 @@ import {Coordinate} from '../utils.js'; export class BubbleDragStrategy implements IDragStrategy { private startLoc: Coordinate | null = null; - /** Was there already an event group in progress when the drag started? */ - private inGroup: boolean = false; - constructor( private bubble: IBubble, private workspace: WorkspaceSvg, @@ -26,10 +22,6 @@ export class BubbleDragStrategy implements IDragStrategy { } startDrag(): void { - this.inGroup = !!eventUtils.getGroup(); - if (!this.inGroup) { - eventUtils.setGroup(true); - } this.startLoc = this.bubble.getRelativeToSurfaceXY(); this.workspace.setResizesEnabled(false); this.workspace.getLayerManager()?.moveToDragLayer(this.bubble); @@ -44,9 +36,6 @@ export class BubbleDragStrategy implements IDragStrategy { endDrag(): void { this.workspace.setResizesEnabled(true); - if (!this.inGroup) { - eventUtils.setGroup(false); - } this.workspace .getLayerManager() diff --git a/core/dragging/comment_drag_strategy.ts b/core/dragging/comment_drag_strategy.ts index 9e051d5bc7c..b7974d8b4ca 100644 --- a/core/dragging/comment_drag_strategy.ts +++ b/core/dragging/comment_drag_strategy.ts @@ -18,9 +18,6 @@ export class CommentDragStrategy implements IDragStrategy { private workspace: WorkspaceSvg; - /** Was there already an event group in progress when the drag started? */ - private inGroup: boolean = false; - constructor(private comment: RenderedWorkspaceComment) { this.workspace = comment.workspace; } @@ -34,10 +31,6 @@ export class CommentDragStrategy implements IDragStrategy { } startDrag(): void { - this.inGroup = !!eventUtils.getGroup(); - if (!this.inGroup) { - eventUtils.setGroup(true); - } this.fireDragStartEvent(); this.startLoc = this.comment.getRelativeToSurfaceXY(); this.workspace.setResizesEnabled(false); @@ -61,9 +54,6 @@ export class CommentDragStrategy implements IDragStrategy { this.comment.snapToGrid(); this.workspace.setResizesEnabled(true); - if (!this.inGroup) { - eventUtils.setGroup(false); - } } /** Fire a UI event at the start of a comment drag. */ diff --git a/core/dragging/dragger.ts b/core/dragging/dragger.ts index 8a9ac87c6a9..518351d5c86 100644 --- a/core/dragging/dragger.ts +++ b/core/dragging/dragger.ts @@ -31,6 +31,9 @@ export class Dragger implements IDragger { /** Handles any drag startup. */ onDragStart(e: PointerEvent) { + if (!eventUtils.getGroup()) { + eventUtils.setGroup(true); + } this.draggable.startDrag(e); } @@ -119,13 +122,13 @@ export class Dragger implements IDragger { this.draggable.endDrag(e); if (wouldDelete && isDeletable(root)) { - // We want to make sure the delete gets grouped with any possible - // move event. - const newGroup = eventUtils.getGroup(); + // We want to make sure the delete gets grouped with any possible move + // event. In core Blockly this shouldn't happen, but due to a change + // in behavior older custom draggables might still clear the group. eventUtils.setGroup(origGroup); root.dispose(); - eventUtils.setGroup(newGroup); } + eventUtils.setGroup(false); } // We need to special case blocks for now so that we look at the root block From 22dbd75bd49999515179f1610d64e9d4c31c2f9e Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 24 Feb 2025 08:17:38 -0800 Subject: [PATCH 100/222] refactor: make CommentView more amenable to subclassing. (#8783) --- core/comments/comment_view.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/comments/comment_view.ts b/core/comments/comment_view.ts index f06c96f80ef..26623d40f74 100644 --- a/core/comments/comment_view.ts +++ b/core/comments/comment_view.ts @@ -95,10 +95,10 @@ export class CommentView implements IRenderedElement { private resizePointerMoveListener: browserEvents.Data | null = null; /** Whether this comment view is currently being disposed or not. */ - private disposing = false; + protected disposing = false; /** Whether this comment view has been disposed or not. */ - private disposed = false; + protected disposed = false; /** Size of this comment when the resize drag was initiated. */ private preResizeSize?: Size; @@ -106,7 +106,7 @@ export class CommentView implements IRenderedElement { /** The default size of newly created comments. */ static defaultCommentSize = new Size(120, 100); - constructor(private readonly workspace: WorkspaceSvg) { + constructor(readonly workspace: WorkspaceSvg) { this.svgRoot = dom.createSvgElement(Svg.G, { 'class': 'blocklyComment blocklyEditable blocklyDraggable', }); From 0ed6c82acca280816069613a059e3830147f132b Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 27 Feb 2025 10:55:34 -0800 Subject: [PATCH 101/222] fix: Disallow and ignore x and y attributes for blocks in toolbox definitions. (#8785) * fix: Disallow and ignore x and y attributes for blocks in toolbox definitions. * chore: Clarify comment in BlockFlyoutInflater. --- core/block_flyout_inflater.ts | 9 +++++++++ core/utils/toolbox.ts | 2 -- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/block_flyout_inflater.ts b/core/block_flyout_inflater.ts index 6011b150eba..b180dbc0c4d 100644 --- a/core/block_flyout_inflater.ts +++ b/core/block_flyout_inflater.ts @@ -101,6 +101,15 @@ export class BlockFlyoutInflater implements IFlyoutInflater { ) { blockDefinition['disabledReasons'] = [MANUALLY_DISABLED]; } + // These fields used to be allowed and may still be present, but are + // ignored here since everything in the flyout should always be laid out + // linearly. + if ('x' in blockDefinition) { + delete blockDefinition['x']; + } + if ('y' in blockDefinition) { + delete blockDefinition['y']; + } block = blocks.appendInternal(blockDefinition as blocks.State, workspace); } diff --git a/core/utils/toolbox.ts b/core/utils/toolbox.ts index 296bb6dcc94..f81ebdc72ca 100644 --- a/core/utils/toolbox.ts +++ b/core/utils/toolbox.ts @@ -24,8 +24,6 @@ export interface BlockInfo { disabledReasons?: string[]; enabled?: boolean; id?: string; - x?: number; - y?: number; collapsed?: boolean; inline?: boolean; data?: string; From fa4fce5c12f8197b6459537cbf0e20249e545e36 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 27 Feb 2025 14:00:40 -0800 Subject: [PATCH 102/222] feat!: Added support for separators in menus. (#8767) * feat!: Added support for separators in menus. * chore: Do English gooder. * fix: Remove menu separators from the DOM during dispose. --- core/contextmenu.ts | 6 +++ core/contextmenu_registry.ts | 93 ++++++++++++++++++++++++++++++------ core/css.ts | 8 ++++ core/extensions.ts | 5 +- core/field_dropdown.ts | 51 +++++++++++--------- core/menu.ts | 28 +++++++---- core/menu_separator.ts | 38 +++++++++++++++ core/utils/aria.ts | 3 ++ 8 files changed, 185 insertions(+), 47 deletions(-) create mode 100644 core/menu_separator.ts diff --git a/core/contextmenu.ts b/core/contextmenu.ts index b49dcba51c0..7123198c2d8 100644 --- a/core/contextmenu.ts +++ b/core/contextmenu.ts @@ -18,6 +18,7 @@ import type { import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import {Menu} from './menu.js'; +import {MenuSeparator} from './menu_separator.js'; import {MenuItem} from './menuitem.js'; import * as serializationBlocks from './serialization/blocks.js'; import * as aria from './utils/aria.js'; @@ -111,6 +112,11 @@ function populate_( menu.setRole(aria.Role.MENU); for (let i = 0; i < options.length; i++) { const option = options[i]; + if (option.separator) { + menu.addChild(new MenuSeparator()); + continue; + } + const menuItem = new MenuItem(option.text); menuItem.setRightToLeft(rtl); menuItem.setRole(aria.Role.MENUITEM); diff --git a/core/contextmenu_registry.ts b/core/contextmenu_registry.ts index fb0d899d141..5bfb1eb63ec 100644 --- a/core/contextmenu_registry.ts +++ b/core/contextmenu_registry.ts @@ -87,21 +87,37 @@ export class ContextMenuRegistry { const menuOptions: ContextMenuOption[] = []; for (const item of this.registeredItems.values()) { if (scopeType === item.scopeType) { - const precondition = item.preconditionFn(scope); - if (precondition !== 'hidden') { + let menuOption: + | ContextMenuRegistry.CoreContextMenuOption + | ContextMenuRegistry.SeparatorContextMenuOption + | ContextMenuRegistry.ActionContextMenuOption; + menuOption = { + scope, + weight: item.weight, + }; + + if (item.separator) { + menuOption = { + ...menuOption, + separator: true, + }; + } else { + const precondition = item.preconditionFn(scope); + if (precondition === 'hidden') continue; + const displayText = typeof item.displayText === 'function' ? item.displayText(scope) : item.displayText; - const menuOption: ContextMenuOption = { + menuOption = { + ...menuOption, text: displayText, - enabled: precondition === 'enabled', callback: item.callback, - scope, - weight: item.weight, + enabled: precondition === 'enabled', }; - menuOptions.push(menuOption); } + + menuOptions.push(menuOption); } } menuOptions.sort(function (a, b) { @@ -134,9 +150,18 @@ export namespace ContextMenuRegistry { } /** - * A menu item as entered in the registry. + * Fields common to all context menu registry items. */ - export interface RegistryItem { + interface CoreRegistryItem { + scopeType: ScopeType; + weight: number; + id: string; + } + + /** + * A representation of a normal, clickable menu item in the registry. + */ + interface ActionRegistryItem extends CoreRegistryItem { /** * @param scope Object that provides a reference to the thing that had its * context menu opened. @@ -144,17 +169,38 @@ export namespace ContextMenuRegistry { * the event that triggered the click on the option. */ callback: (scope: Scope, e: PointerEvent) => void; - scopeType: ScopeType; displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement; preconditionFn: (p1: Scope) => string; + separator?: never; + } + + /** + * A representation of a menu separator item in the registry. + */ + interface SeparatorRegistryItem extends CoreRegistryItem { + separator: true; + callback?: never; + displayText?: never; + preconditionFn?: never; + } + + /** + * A menu item as entered in the registry. + */ + export type RegistryItem = ActionRegistryItem | SeparatorRegistryItem; + + /** + * Fields common to all context menu items as used by contextmenu.ts. + */ + export interface CoreContextMenuOption { + scope: Scope; weight: number; - id: string; } /** - * A menu item as presented to contextmenu.js. + * A representation of a normal, clickable menu item in contextmenu.ts. */ - export interface ContextMenuOption { + export interface ActionContextMenuOption extends CoreContextMenuOption { text: string | HTMLElement; enabled: boolean; /** @@ -164,10 +210,26 @@ export namespace ContextMenuRegistry { * the event that triggered the click on the option. */ callback: (scope: Scope, e: PointerEvent) => void; - scope: Scope; - weight: number; + separator?: never; } + /** + * A representation of a menu separator item in contextmenu.ts. + */ + export interface SeparatorContextMenuOption extends CoreContextMenuOption { + separator: true; + text?: never; + enabled?: never; + callback?: never; + } + + /** + * A menu item as presented to contextmenu.ts. + */ + export type ContextMenuOption = + | ActionContextMenuOption + | SeparatorContextMenuOption; + /** * A subset of ContextMenuOption corresponding to what was publicly * documented. ContextMenuOption should be preferred for new code. @@ -176,6 +238,7 @@ export namespace ContextMenuRegistry { text: string; enabled: boolean; callback: (p1: Scope) => void; + separator?: never; } /** diff --git a/core/css.ts b/core/css.ts index 57217f85483..00bbc0e0261 100644 --- a/core/css.ts +++ b/core/css.ts @@ -461,6 +461,14 @@ input[type=number] { margin-right: -24px; } +.blocklyMenuSeparator { + background-color: #ccc; + height: 1px; + border: 0; + margin-left: 4px; + margin-right: 4px; +} + .blocklyBlockDragSurface, .blocklyAnimationLayer { position: absolute; top: 0; diff --git a/core/extensions.ts b/core/extensions.ts index 0957b7f86ca..59d218d17fa 100644 --- a/core/extensions.ts +++ b/core/extensions.ts @@ -437,7 +437,10 @@ function checkDropdownOptionsInTable( } const options = dropdown.getOptions(); - for (const [, key] of options) { + for (const option of options) { + if (option === FieldDropdown.SEPARATOR) continue; + + const [, key] = option; if (lookupTable[key] === undefined) { console.warn( `No tooltip mapping for value ${key} of field ` + diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 0be621d383c..60977524af7 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -23,6 +23,7 @@ import { } from './field.js'; import * as fieldRegistry from './field_registry.js'; import {Menu} from './menu.js'; +import {MenuSeparator} from './menu_separator.js'; import {MenuItem} from './menuitem.js'; import * as aria from './utils/aria.js'; import {Coordinate} from './utils/coordinate.js'; @@ -35,14 +36,10 @@ import {Svg} from './utils/svg.js'; * Class for an editable dropdown field. */ export class FieldDropdown extends Field { - /** Horizontal distance that a checkmark overhangs the dropdown. */ - static CHECKMARK_OVERHANG = 25; - /** - * Maximum height of the dropdown menu, as a percentage of the viewport - * height. + * Magic constant used to represent a separator in a list of dropdown items. */ - static MAX_MENU_HEIGHT_VH = 0.45; + static readonly SEPARATOR = 'separator'; static ARROW_CHAR = '▾'; @@ -323,7 +320,13 @@ export class FieldDropdown extends Field { const options = this.getOptions(false); this.selectedMenuItem = null; for (let i = 0; i < options.length; i++) { - const [label, value] = options[i]; + const option = options[i]; + if (option === FieldDropdown.SEPARATOR) { + menu.addChild(new MenuSeparator()); + continue; + } + + const [label, value] = option; const content = (() => { if (typeof label === 'object') { // Convert ImageProperties to an HTMLImageElement. @@ -667,7 +670,10 @@ export class FieldDropdown extends Field { suffix?: string; } { let hasImages = false; - const trimmedOptions = options.map(([label, value]): MenuOption => { + const trimmedOptions = options.map((option): MenuOption => { + if (option === FieldDropdown.SEPARATOR) return option; + + const [label, value] = option; if (typeof label === 'string') { return [parsing.replaceMessageReferences(label), value]; } @@ -748,28 +754,28 @@ export class FieldDropdown extends Field { } let foundError = false; for (let i = 0; i < options.length; i++) { - const tuple = options[i]; - if (!Array.isArray(tuple)) { + const option = options[i]; + if (!Array.isArray(option) && option !== FieldDropdown.SEPARATOR) { foundError = true; console.error( - `Invalid option[${i}]: Each FieldDropdown option must be an array. - Found: ${tuple}`, + `Invalid option[${i}]: Each FieldDropdown option must be an array or + the string literal 'separator'. Found: ${option}`, ); - } else if (typeof tuple[1] !== 'string') { + } else if (typeof option[1] !== 'string') { foundError = true; console.error( `Invalid option[${i}]: Each FieldDropdown option id must be a string. - Found ${tuple[1]} in: ${tuple}`, + Found ${option[1]} in: ${option}`, ); } else if ( - tuple[0] && - typeof tuple[0] !== 'string' && - typeof tuple[0].src !== 'string' + option[0] && + typeof option[0] !== 'string' && + typeof option[0].src !== 'string' ) { foundError = true; console.error( `Invalid option[${i}]: Each FieldDropdown option must have a string - label or image description. Found ${tuple[0]} in: ${tuple}`, + label or image description. Found ${option[0]} in: ${option}`, ); } } @@ -790,11 +796,12 @@ export interface ImageProperties { } /** - * An individual option in the dropdown menu. The first element is the human- - * readable value (text or image), and the second element is the language- - * neutral value. + * An individual option in the dropdown menu. Can be either the string literal + * `separator` for a menu separator item, or an array for normal action menu + * items. In the latter case, the first element is the human-readable value + * (text or image), and the second element is the language-neutral value. */ -export type MenuOption = [string | ImageProperties, string]; +export type MenuOption = [string | ImageProperties, string] | 'separator'; /** * A function that generates an array of menu options for FieldDropdown diff --git a/core/menu.ts b/core/menu.ts index 7746bb2127f..acb5a378034 100644 --- a/core/menu.ts +++ b/core/menu.ts @@ -12,7 +12,8 @@ // Former goog.module ID: Blockly.Menu import * as browserEvents from './browser_events.js'; -import type {MenuItem} from './menuitem.js'; +import type {MenuSeparator} from './menu_separator.js'; +import {MenuItem} from './menuitem.js'; import * as aria from './utils/aria.js'; import {Coordinate} from './utils/coordinate.js'; import type {Size} from './utils/size.js'; @@ -23,11 +24,9 @@ import * as style from './utils/style.js'; */ export class Menu { /** - * Array of menu items. - * (Nulls are never in the array, but typing the array as nullable prevents - * the compiler from objecting to .indexOf(null)) + * Array of menu items and separators. */ - private readonly menuItems: MenuItem[] = []; + private readonly menuItems: Array = []; /** * Coordinates of the mousedown event that caused this menu to open. Used to @@ -69,10 +68,10 @@ export class Menu { /** * Add a new menu item to the bottom of this menu. * - * @param menuItem Menu item to append. + * @param menuItem Menu item or separator to append. * @internal */ - addChild(menuItem: MenuItem) { + addChild(menuItem: MenuItem | MenuSeparator) { this.menuItems.push(menuItem); } @@ -227,7 +226,8 @@ export class Menu { while (currentElement && currentElement !== menuElem) { if (currentElement.classList.contains('blocklyMenuItem')) { // Having found a menu item's div, locate that menu item in this menu. - for (let i = 0, menuItem; (menuItem = this.menuItems[i]); i++) { + const items = this.getMenuItems(); + for (let i = 0, menuItem; (menuItem = items[i]); i++) { if (menuItem.getElement() === currentElement) { return menuItem; } @@ -309,7 +309,8 @@ export class Menu { private highlightHelper(startIndex: number, delta: number) { let index = startIndex + delta; let menuItem; - while ((menuItem = this.menuItems[index])) { + const items = this.getMenuItems(); + while ((menuItem = items[index])) { if (menuItem.isEnabled()) { this.setHighlighted(menuItem); break; @@ -459,4 +460,13 @@ export class Menu { menuSize.height = menuDom.scrollHeight; return menuSize; } + + /** + * Returns the action menu items (omitting separators) in this menu. + * + * @returns The MenuItem objects displayed in this menu. + */ + private getMenuItems(): MenuItem[] { + return this.menuItems.filter((item) => item instanceof MenuItem); + } } diff --git a/core/menu_separator.ts b/core/menu_separator.ts new file mode 100644 index 00000000000..6f7f468ad62 --- /dev/null +++ b/core/menu_separator.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as aria from './utils/aria.js'; + +/** + * Representation of a section separator in a menu. + */ +export class MenuSeparator { + /** + * DOM element representing this separator in a menu. + */ + private element: HTMLHRElement | null = null; + + /** + * Creates the DOM representation of this separator. + * + * @returns An
element. + */ + createDom(): HTMLHRElement { + this.element = document.createElement('hr'); + this.element.className = 'blocklyMenuSeparator'; + aria.setRole(this.element, aria.Role.SEPARATOR); + + return this.element; + } + + /** + * Disposes of this separator. + */ + dispose() { + this.element?.remove(); + this.element = null; + } +} diff --git a/core/utils/aria.ts b/core/utils/aria.ts index 567ea95ef73..8089298e4ec 100644 --- a/core/utils/aria.ts +++ b/core/utils/aria.ts @@ -48,6 +48,9 @@ export enum Role { // ARIA role for a tree item that sometimes may be expanded or collapsed. TREEITEM = 'treeitem', + + // ARIA role for a visual separator in e.g. a menu. + SEPARATOR = 'separator', } /** From 00d77456c9123ce0ad9d0aa397ae55970c0b3220 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 12 Mar 2025 09:27:47 -0700 Subject: [PATCH 103/222] =?UTF-8?q?Revert=20"fix!:=20Remove=20the=20blockl?= =?UTF-8?q?yMenuItemHighlight=20CSS=20class=20and=20use=20the=20hover?= =?UTF-8?q?=E2=80=A6"=20(#8800)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d6125d4fb94ac7f4ab9e57a2944a5aa1c6ead328. --- core/css.ts | 3 ++- core/menu.ts | 2 ++ core/menuitem.ts | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/core/css.ts b/core/css.ts index 00bbc0e0261..51d410d0b62 100644 --- a/core/css.ts +++ b/core/css.ts @@ -438,7 +438,8 @@ input[type=number] { cursor: inherit; } -.blocklyMenuItem:hover { +/* State: hover. */ +.blocklyMenuItemHighlight { background-color: rgba(0,0,0,.1); } diff --git a/core/menu.ts b/core/menu.ts index acb5a378034..6baab214959 100644 --- a/core/menu.ts +++ b/core/menu.ts @@ -249,9 +249,11 @@ export class Menu { setHighlighted(item: MenuItem | null) { const currentHighlighted = this.highlightedItem; if (currentHighlighted) { + currentHighlighted.setHighlighted(false); this.highlightedItem = null; } if (item) { + item.setHighlighted(true); this.highlightedItem = item; // Bring the highlighted item into view. This has no effect if the menu is // not scrollable. diff --git a/core/menuitem.ts b/core/menuitem.ts index 7136d3f4996..ebeb9404bdd 100644 --- a/core/menuitem.ts +++ b/core/menuitem.ts @@ -12,6 +12,7 @@ // Former goog.module ID: Blockly.MenuItem import * as aria from './utils/aria.js'; +import * as dom from './utils/dom.js'; import * as idGenerator from './utils/idgenerator.js'; /** @@ -67,6 +68,7 @@ export class MenuItem { 'blocklyMenuItem ' + (this.enabled ? '' : 'blocklyMenuItemDisabled ') + (this.checked ? 'blocklyMenuItemSelected ' : '') + + (this.highlight ? 'blocklyMenuItemHighlight ' : '') + (this.rightToLeft ? 'blocklyMenuItemRtl ' : ''); const content = document.createElement('div'); @@ -175,6 +177,25 @@ export class MenuItem { this.checked = checked; } + /** + * Highlights or unhighlights the component. + * + * @param highlight Whether to highlight or unhighlight the component. + * @internal + */ + setHighlighted(highlight: boolean) { + this.highlight = highlight; + const el = this.getElement(); + if (el && this.isEnabled()) { + const name = 'blocklyMenuItemHighlight'; + if (highlight) { + dom.addClass(el, name); + } else { + dom.removeClass(el, name); + } + } + } + /** * Returns true if the menu item is enabled, false otherwise. * From 629d6e05bb17526b72c2d538727da89c7fae681e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:45:05 +0000 Subject: [PATCH 104/222] chore(deps): bump eslint-config-prettier from 9.1.0 to 10.1.1 Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 9.1.0 to 10.1.1. - [Release notes](https://github.com/prettier/eslint-config-prettier/releases) - [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-config-prettier/compare/v9.1.0...v10.1.1) --- updated-dependencies: - dependency-name: eslint-config-prettier dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 68859c47385..260347cc5b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "concurrently": "^9.0.1", "eslint": "^9.15.0", "eslint-config-google": "^0.14.0", - "eslint-config-prettier": "^9.1.0", + "eslint-config-prettier": "^10.1.1", "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", @@ -3877,10 +3877,11 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", + "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, diff --git a/package.json b/package.json index 286e0439704..74bc444912c 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "concurrently": "^9.0.1", "eslint": "^9.15.0", "eslint-config-google": "^0.14.0", - "eslint-config-prettier": "^9.1.0", + "eslint-config-prettier": "^10.1.1", "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", From 0f07567965955db76a05b0295a56c78679489dd5 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 20 Mar 2025 09:46:31 -0700 Subject: [PATCH 105/222] fix: Allow the marker's current node to be null. (#8802) --- core/keyboard_nav/marker.ts | 39 +++++++++-------------------- core/marker_manager.ts | 15 ++++++----- core/renderers/common/marker_svg.ts | 4 +-- 3 files changed, 21 insertions(+), 37 deletions(-) diff --git a/core/keyboard_nav/marker.ts b/core/keyboard_nav/marker.ts index e3b438e6efe..aaa8e7355db 100644 --- a/core/keyboard_nav/marker.ts +++ b/core/keyboard_nav/marker.ts @@ -24,24 +24,17 @@ export class Marker { colour: string | null = null; /** The current location of the marker. */ - // AnyDuringMigration because: Type 'null' is not assignable to type - // 'ASTNode'. - private curNode: ASTNode = null as AnyDuringMigration; + private curNode: ASTNode | null = null; /** * The object in charge of drawing the visual representation of the current * node. */ - // AnyDuringMigration because: Type 'null' is not assignable to type - // 'MarkerSvg'. - private drawer: MarkerSvg = null as AnyDuringMigration; + private drawer: MarkerSvg | null = null; /** The type of the marker. */ type = 'marker'; - /** Constructs a new Marker instance. */ - constructor() {} - /** * Sets the object in charge of drawing the marker. * @@ -56,7 +49,7 @@ export class Marker { * * @returns The object in charge of drawing the marker. */ - getDrawer(): MarkerSvg { + getDrawer(): MarkerSvg | null { return this.drawer; } @@ -65,23 +58,19 @@ export class Marker { * * @returns The current field, connection, or block the marker is on. */ - getCurNode(): ASTNode { + getCurNode(): ASTNode | null { return this.curNode; } /** * Set the location of the marker and call the update method. - * Setting isStack to true will only work if the newLocation is the top most - * output or previous connection on a stack. * - * @param newNode The new location of the marker. + * @param newNode The new location of the marker, or null to remove it. */ - setCurNode(newNode: ASTNode) { + setCurNode(newNode: ASTNode | null) { const oldNode = this.curNode; this.curNode = newNode; - if (this.drawer) { - this.drawer.draw(oldNode, this.curNode); - } + this.drawer?.draw(oldNode, this.curNode); } /** @@ -90,22 +79,18 @@ export class Marker { * @internal */ draw() { - if (this.drawer) { - this.drawer.draw(this.curNode, this.curNode); - } + this.drawer?.draw(this.curNode, this.curNode); } /** Hide the marker SVG. */ hide() { - if (this.drawer) { - this.drawer.hide(); - } + this.drawer?.hide(); } /** Dispose of this marker. */ dispose() { - if (this.getDrawer()) { - this.getDrawer().dispose(); - } + this.drawer?.dispose(); + this.drawer = null; + this.curNode = null; } } diff --git a/core/marker_manager.ts b/core/marker_manager.ts index d7035534da7..51183242d3d 100644 --- a/core/marker_manager.ts +++ b/core/marker_manager.ts @@ -50,10 +50,11 @@ export class MarkerManager { if (this.markers.has(id)) { this.unregisterMarker(id); } - marker.setDrawer( - this.workspace.getRenderer().makeMarkerDrawer(this.workspace, marker), - ); - this.setMarkerSvg(marker.getDrawer().createDom()); + const drawer = this.workspace + .getRenderer() + .makeMarkerDrawer(this.workspace, marker); + marker.setDrawer(drawer); + this.setMarkerSvg(drawer.createDom()); this.markers.set(id, marker); } @@ -104,16 +105,14 @@ export class MarkerManager { * @param cursor The cursor used to move around this workspace. */ setCursor(cursor: Cursor) { - if (this.cursor && this.cursor.getDrawer()) { - this.cursor.getDrawer().dispose(); - } + this.cursor?.getDrawer()?.dispose(); this.cursor = cursor; if (this.cursor) { const drawer = this.workspace .getRenderer() .makeMarkerDrawer(this.workspace, this.cursor); this.cursor.setDrawer(drawer); - this.setCursorSvg(this.cursor.getDrawer().createDom()); + this.setCursorSvg(drawer.createDom()); } } diff --git a/core/renderers/common/marker_svg.ts b/core/renderers/common/marker_svg.ts index 057324f0346..77d35883c3e 100644 --- a/core/renderers/common/marker_svg.ts +++ b/core/renderers/common/marker_svg.ts @@ -156,7 +156,7 @@ export class MarkerSvg { * @param oldNode The previous node the marker was on or null. * @param curNode The node that we want to draw the marker for. */ - draw(oldNode: ASTNode, curNode: ASTNode) { + draw(oldNode: ASTNode | null, curNode: ASTNode | null) { if (!curNode) { this.hide(); return; @@ -620,7 +620,7 @@ export class MarkerSvg { * @param oldNode The old node the marker used to be on. * @param curNode The new node the marker is currently on. */ - protected fireMarkerEvent(oldNode: ASTNode, curNode: ASTNode) { + protected fireMarkerEvent(oldNode: ASTNode | null, curNode: ASTNode) { const curBlock = curNode.getSourceBlock(); const event = new (eventUtils.get(EventType.MARKER_MOVE))( curBlock, From e02d3853ee5e67d019efe537afcbc134a28abca3 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 20 Mar 2025 14:37:18 -0700 Subject: [PATCH 106/222] release: Update version number to 12.0.0-beta.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b66777d14b..66b6b3794b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockly", - "version": "12.0.0-beta.1", + "version": "12.0.0-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "12.0.0-beta.1", + "version": "12.0.0-beta.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 1f5260b4d55..62fbeb2e08f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "12.0.0-beta.1", + "version": "12.0.0-beta.2", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly" From d9beacddb4db6c423fbe58b027e9f14393ae3ffd Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Mar 2025 00:33:51 +0000 Subject: [PATCH 107/222] feat: add FocusManager This is the bulk of the work for introducing the central logical unit for managing and sychronizing focus as a first-class Blockly concept with that of DOM focus. There's a lot to do yet, including: - Ensuring clicks within Blockly's scope correctly sync back to focus changes. - Adding support for, and testing, cases when focus is lost from all registered trees. - Testing nested tree propagation. - Testing the traverser utility class. - Adding implementations for IFocusableTree and IFocusableNode throughout Blockly. --- core/blockly.ts | 3 + core/css.ts | 9 + core/focus_manager.ts | 295 ++ core/interfaces/i_focusable_node.ts | 5 +- core/interfaces/i_focusable_tree.ts | 2 + core/utils/focusable_tree_traverser.ts | 84 + tests/mocha/focus_manager_test.js | 3919 ++++++++++++++++++++++++ tests/mocha/index.html | 73 + 8 files changed, 4389 insertions(+), 1 deletion(-) create mode 100644 core/focus_manager.ts create mode 100644 core/utils/focusable_tree_traverser.ts create mode 100644 tests/mocha/focus_manager_test.js diff --git a/core/blockly.ts b/core/blockly.ts index cf77bca3fb8..c29961f591f 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -106,6 +106,7 @@ import {FlyoutItem} from './flyout_item.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import {FlyoutSeparator} from './flyout_separator.js'; import {VerticalFlyout} from './flyout_vertical.js'; +import {FocusManager, getFocusManager} from './focus_manager.js'; import {CodeGenerator} from './generator.js'; import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; @@ -521,6 +522,7 @@ export { FlyoutItem, FlyoutMetricsManager, FlyoutSeparator, + FocusManager, CodeGenerator as Generator, Gesture, Grid, @@ -607,6 +609,7 @@ export { WorkspaceSvg, ZoomControls, config, + getFocusManager, hasBubble, icons, inject, diff --git a/core/css.ts b/core/css.ts index 57217f85483..4ebb4e26074 100644 --- a/core/css.ts +++ b/core/css.ts @@ -484,4 +484,13 @@ input[type=number] { .blocklyDragging .blocklyIconGroup { cursor: grabbing; } + +.blocklyActiveFocus { + outline-color: #2ae; + outline-width: 2px; +} +.blocklyPassiveFocus { + outline-color: #3fdfff; + outline-width: 1.5px; +} `; diff --git a/core/focus_manager.ts b/core/focus_manager.ts new file mode 100644 index 00000000000..5e6e0af48ab --- /dev/null +++ b/core/focus_manager.ts @@ -0,0 +1,295 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; +import * as dom from './utils/dom.js'; + +/** + * Type declaration for returning focus to FocusManager upon completing an + * ephemeral UI flow (such as a dialog). + * + * See FocusManager.takeEphemeralFocus for more details. + */ +export type ReturnEphemeralFocus = () => void; + +/** + * A per-page singleton that manages Blockly focus across one or more + * IFocusableTrees, and bidirectionally synchronizes this focus with the DOM. + * + * Callers that wish to explicitly change input focus for select Blockly + * components on the page should use the focus functions in this manager. + * + * The manager is responsible for handling focus events from the DOM (which may + * may arise from users clicking on page elements) and ensuring that + * corresponding IFocusableNodes are clearly marked as actively/passively + * highlighted in the same way that this would be represented with calls to + * focusNode(). + */ +export class FocusManager { + focusedNode: IFocusableNode | null = null; + registeredTrees: Array = []; + + private currentlyHoldsEphemeralFocus: boolean = false; + + constructor( + addGlobalEventListener: (type: string, listener: EventListener) => void, + ) { + // Register root document focus listeners for tracking when focus leaves all + // tracked focusable trees. + addGlobalEventListener('focusin', (event) => { + if (!(event instanceof FocusEvent)) return; + + // The target that now has focus. + const activeElement = document.activeElement; + let newNode: IFocusableNode | null = null; + if ( + activeElement instanceof HTMLElement || + activeElement instanceof SVGElement + ) { + // If the target losing focus maps to any tree, then it should be + // updated. Per the contract of findFocusableNodeFor only one tree + // should claim the element. + const matchingNodes = this.registeredTrees.map((tree) => + tree.findFocusableNodeFor(activeElement), + ); + newNode = matchingNodes.find((node) => !!node) ?? null; + } + + if (newNode) { + this.focusNode(newNode); + } else { + // TODO: Set previous to passive if all trees are losing active focus. + } + }); + } + + /** + * Registers a new IFocusableTree for automatic focus management. + * + * If the tree currently has an element with DOM focus, it will not affect the + * internal state in this manager until the focus changes to a new, + * now-monitored element/node. + * + * This function throws if the provided tree is already currently registered + * in this manager. Use isRegistered to check in cases when it can't be + * certain whether the tree has been registered. + */ + registerTree(tree: IFocusableTree): void { + if (this.isRegistered(tree)) { + throw Error(`Attempted to re-register already registered tree: ${tree}.`); + } + this.registeredTrees.push(tree); + } + + /** + * Returns whether the specified tree has already been registered in this + * manager using registerTree and hasn't yet been unregistered using + * unregisterTree. + */ + isRegistered(tree: IFocusableTree): boolean { + return this.registeredTrees.findIndex((reg) => reg == tree) !== -1; + } + + /** + * Unregisters a IFocusableTree from automatic focus management. + * + * If the tree had a previous focused node, it will have its highlight + * removed. This function does NOT change DOM focus. + * + * This function throws if the provided tree is not currently registered in + * this manager. + */ + unregisterTree(tree: IFocusableTree): void { + if (!this.isRegistered(tree)) { + throw Error(`Attempted to unregister not registered tree: ${tree}.`); + } + const treeIndex = this.registeredTrees.findIndex((tree) => tree == tree); + this.registeredTrees.splice(treeIndex, 1); + + const focusedNode = tree.getFocusedNode(); + const root = tree.getRootFocusableNode(); + if (focusedNode != null) this.removeHighlight(focusedNode); + if (this.focusedNode == focusedNode || this.focusedNode == root) { + this.focusedNode = null; + } + this.removeHighlight(root); + } + + /** + * Returns the current IFocusableTree that has focus, or null if none + * currently do. + * + * Note also that if ephemeral focus is currently captured (e.g. using + * takeEphemeralFocus) then the returned tree here may not currently have DOM + * focus. + */ + getFocusedTree(): IFocusableTree | null { + return this.focusedNode?.getFocusableTree() ?? null; + } + + /** + * Returns the current IFocusableNode with focus (which is always tied to a + * focused IFocusableTree), or null if there isn't one. + * + * Note that this function will maintain parity with + * IFocusableTree.getFocusedNode(). That is, if a tree itself has focus but + * none of its non-root children do, this will return null but + * getFocusedTree() will not. + * + * Note also that if ephemeral focus is currently captured (e.g. using + * takeEphemeralFocus) then the returned node here may not currently have DOM + * focus. + */ + getFocusedNode(): IFocusableNode | null { + return this.focusedNode; + } + + /** + * Focuses the specific IFocusableTree. This either means restoring active + * focus to the tree's passively focused node, or focusing the tree's root + * node. + * + * Note that if the specified tree already has a focused node then this will + * not change any existing focus (unless that node has passive focus, then it + * will be restored to active focus). + * + * See getFocusedNode for details on how other nodes are affected. + * + * @param focusableTree The tree that should receive active + * focus. + */ + focusTree(focusableTree: IFocusableTree): void { + if (!this.isRegistered(focusableTree)) { + throw Error(`Attempted to focus unregistered tree: ${focusableTree}.`); + } + this.focusNode( + focusableTree.getFocusedNode() ?? focusableTree.getRootFocusableNode(), + ); + } + + /** + * Focuses DOM input on the selected node, and marks it as actively focused. + * + * Any previously focused node will be updated to be passively highlighted (if + * it's in a different focusable tree) or blurred (if it's in the same one). + * + * @param focusableNode The node that should receive active + * focus. + */ + focusNode(focusableNode: IFocusableNode): void { + const curTree = focusableNode.getFocusableTree(); + if (!this.isRegistered(curTree)) { + throw Error(`Attempted to focus unregistered node: ${focusableNode}.`); + } + const prevNode = this.focusedNode; + if (prevNode && prevNode.getFocusableTree() !== curTree) { + this.setNodeToPassive(prevNode); + } + // If there's a focused node in the new node's tree, ensure it's reset. + const prevNodeCurTree = curTree.getFocusedNode(); + const curTreeRoot = curTree.getRootFocusableNode(); + if (prevNodeCurTree) { + this.removeHighlight(prevNodeCurTree); + } + // For caution, ensure that the root is always reset since getFocusedNode() + // is expected to return null if the root was highlighted, if the root is + // not the node now being set to active. + if (curTreeRoot !== focusableNode) { + this.removeHighlight(curTreeRoot); + } + if (!this.currentlyHoldsEphemeralFocus) { + // Only change the actively focused node if ephemeral state isn't held. + this.setNodeToActive(focusableNode); + } + this.focusedNode = focusableNode; + } + + /** + * Ephemerally captures focus for a selected element until the returned lambda + * is called. This is expected to be especially useful for ephemeral UI flows + * like dialogs. + * + * IMPORTANT: the returned lambda *must* be called, otherwise automatic focus + * will no longer work anywhere on the page. It is highly recommended to tie + * the lambda call to the closure of the corresponding UI so that if input is + * manually changed to an element outside of the ephemeral UI, the UI should + * close and automatic input restored. Note that this lambda must be called + * exactly once and that subsequent calls will throw an error. + * + * Note that the manager will continue to track DOM input signals even when + * ephemeral focus is active, but it won't actually change node state until + * the returned lambda is called. Additionally, only 1 ephemeral focus context + * can be active at any given time (attempting to activate more than one + * simultaneously will result in an error being thrown). + */ + takeEphemeralFocus( + focusableElement: HTMLElement | SVGElement, + ): ReturnEphemeralFocus { + if (this.currentlyHoldsEphemeralFocus) { + throw Error( + `Attempted to take ephemeral focus when it's already held, ` + + `with new element: ${focusableElement}.`, + ); + } + this.currentlyHoldsEphemeralFocus = true; + + if (this.focusedNode) { + this.setNodeToPassive(this.focusedNode); + } + focusableElement.focus(); + + let hasFinishedEphemeralFocus = false; + return () => { + if (hasFinishedEphemeralFocus) { + throw Error( + `Attempted to finish ephemeral focus twice for element: ` + + `${focusableElement}.`, + ); + } + hasFinishedEphemeralFocus = true; + this.currentlyHoldsEphemeralFocus = false; + + if (this.focusedNode) { + this.setNodeToActive(this.focusedNode); + } + }; + } + + private setNodeToActive(node: IFocusableNode): void { + const element = node.getFocusableElement(); + dom.addClass(element, 'blocklyActiveFocus'); + dom.removeClass(element, 'blocklyPassiveFocus'); + element.focus(); + } + + private setNodeToPassive(node: IFocusableNode): void { + const element = node.getFocusableElement(); + dom.removeClass(element, 'blocklyActiveFocus'); + dom.addClass(element, 'blocklyPassiveFocus'); + } + + private removeHighlight(node: IFocusableNode): void { + const element = node.getFocusableElement(); + dom.removeClass(element, 'blocklyActiveFocus'); + dom.removeClass(element, 'blocklyPassiveFocus'); + } +} + +let focusManager: FocusManager | null = null; + +/** + * Returns the page-global FocusManager. + * + * The returned instance is guaranteed to not change across function calls, but + * may change across page loads. + */ +export function getFocusManager(): FocusManager { + if (!focusManager) { + focusManager = new FocusManager(document.addEventListener); + } + return focusManager; +} diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts index 87a0293ae0c..14100d44c7f 100644 --- a/core/interfaces/i_focusable_node.ts +++ b/core/interfaces/i_focusable_node.ts @@ -20,7 +20,10 @@ export interface IFocusableNode { * - blocklyPassiveFocus * * The returned element must also have a valid ID specified, and unique to the - * element relative to its nearest IFocusableTree parent. + * element relative to its nearest IFocusableTree parent. It must also have a + * negative tabindex (since the focus manager itself will manage its tab index + * and a tab index must be present in order for the element to be focusable in + * the DOM). * * It's expected the return element will not change for the lifetime of the * node. diff --git a/core/interfaces/i_focusable_tree.ts b/core/interfaces/i_focusable_tree.ts index 21f87678d01..1a8ccf82b4a 100644 --- a/core/interfaces/i_focusable_tree.ts +++ b/core/interfaces/i_focusable_tree.ts @@ -46,6 +46,8 @@ export interface IFocusableTree { * * The provided element must have a non-null ID that conforms to the contract * mentioned in IFocusableNode. + * + * This function may match against the root node of the tree. */ findFocusableNodeFor( element: HTMLElement | SVGElement, diff --git a/core/utils/focusable_tree_traverser.ts b/core/utils/focusable_tree_traverser.ts new file mode 100644 index 00000000000..b7465e884b4 --- /dev/null +++ b/core/utils/focusable_tree_traverser.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; + +/** + * A helper utility for IFocusableTree implementations to aid with common + * tree traversals. + */ +export class FocusableTreeTraverser { + /** + * Returns the current IFocusableNode that either has the CSS class + * 'blocklyActiveFocus' or 'blocklyPassiveFocus', only considering HTML and + * SVG elements. + * + * This can match against the tree's root. + * + * @param tree The IFocusableTree in which to search for a focused node. + * @returns The IFocusableNode currently with focus, or null if none. + */ + static findFocusedNode(tree: IFocusableTree): IFocusableNode | null { + const root = tree.getRootFocusableNode().getFocusableElement(); + const activeElem = root.querySelector('.blocklyActiveFocus'); + let active: IFocusableNode | null = null; + if (activeElem instanceof HTMLElement || activeElem instanceof SVGElement) { + active = tree.findFocusableNodeFor(activeElem); + } + const passiveElems = Array.from( + root.querySelectorAll('.blocklyPassiveFocus'), + ); + const passive = passiveElems.map((elem) => { + if (elem instanceof HTMLElement || elem instanceof SVGElement) { + return tree.findFocusableNodeFor(elem); + } else return null; + }); + return active || passive.find((node) => !!node) || null; + } + + /** + * Returns the IFocusableNode corresponding to the specified HTML or SVG + * element iff it's the root element or a descendent of the root element of + * the specified IFocusableTree. + * + * If the tree contains another nested IFocusableTree, the nested tree may be + * traversed but its nodes will never be returned here per the contract of + * findChildById. + * + * findChildById is a provided callback that takes an element ID and maps it + * back to the corresponding IFocusableNode within the provided + * IFocusableTree. These IDs will match the contract specified in the + * documentation for IFocusableNode. This function must not return any node + * that doesn't directly belong to the node's nearest parent tree. + * + * @param element The HTML or SVG element being sought. + * @param tree The tree under which the provided element may be a descendant. + * @param findChildById The ID->IFocusableNode mapping callback that must + * follow the contract mentioned above. + * @returns The matching IFocusableNode, or null if there is no match. + */ + static findFocusableNodeFor( + element: HTMLElement | SVGElement, + tree: IFocusableTree, + findChildById: (id: string) => IFocusableNode | null, + ): IFocusableNode | null { + if (element === tree.getRootFocusableNode().getFocusableElement()) { + return tree.getRootFocusableNode(); + } + const matchedChildNode = findChildById(element.id); + const elementParent = element.parentElement; + if (!matchedChildNode && elementParent) { + // Recurse up to find the nearest tree/node. + return FocusableTreeTraverser.findFocusableNodeFor( + elementParent, + tree, + findChildById, + ); + } + return matchedChildNode; + } +} diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js new file mode 100644 index 00000000000..86a19fd1857 --- /dev/null +++ b/tests/mocha/focus_manager_test.js @@ -0,0 +1,3919 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {FocusManager} from '../../build/src/core/focus_manager.js'; +import {FocusableTreeTraverser} from '../../build/src/core/utils/focusable_tree_traverser.js'; +import {assert} from '../../node_modules/chai/chai.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('FocusManager', function () { + setup(function () { + sharedTestSetup.call(this); + + const testState = this; + const addDocumentEventListener = function (type, listener) { + testState.globalDocumentEventListenerType = type; + testState.globalDocumentEventListener = listener; + document.addEventListener(type, listener); + }; + this.focusManager = new FocusManager(addDocumentEventListener); + + const FocusableNodeImpl = function (element, tree) { + this.getFocusableElement = function () { + return element; + }; + + this.getFocusableTree = function () { + return tree; + }; + }; + const FocusableTreeImpl = function (rootElement) { + this.idToNodeMap = {}; + + this.addNode = function (element) { + const node = new FocusableNodeImpl(element, this); + this.idToNodeMap[element.id] = node; + return node; + }; + + this.getFocusedNode = function () { + return FocusableTreeTraverser.findFocusedNode(this); + }; + + this.getRootFocusableNode = function () { + return this.rootNode; + }; + + this.findFocusableNodeFor = function (element) { + return FocusableTreeTraverser.findFocusableNodeFor( + element, + this, + (id) => this.idToNodeMap[id], + ); + }; + + this.rootNode = this.addNode(rootElement); + }; + + const createFocusableTree = function (rootElementId) { + return new FocusableTreeImpl(document.getElementById(rootElementId)); + }; + const createFocusableNode = function (tree, elementId) { + return tree.addNode(document.getElementById(elementId)); + }; + + this.testFocusableTree1 = createFocusableTree('testFocusableTree1'); + this.testFocusableTree1Node1 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node1', + ); + this.testFocusableTree1Node1Child1 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node1.child1', + ); + this.testFocusableTree1Node2 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node2', + ); + this.testFocusableTree2 = createFocusableTree('testFocusableTree2'); + this.testFocusableTree2Node1 = createFocusableNode( + this.testFocusableTree2, + 'testFocusableTree2.node1', + ); + this.testFocusableGroup1 = createFocusableTree('testFocusableGroup1'); + this.testFocusableGroup1Node1 = createFocusableNode( + this.testFocusableGroup1, + 'testFocusableGroup1.node1', + ); + this.testFocusableGroup1Node1Child1 = createFocusableNode( + this.testFocusableGroup1, + 'testFocusableGroup1.node1.child1', + ); + this.testFocusableGroup1Node2 = createFocusableNode( + this.testFocusableGroup1, + 'testFocusableGroup1.node2', + ); + this.testFocusableGroup2 = createFocusableTree('testFocusableGroup2'); + this.testFocusableGroup2Node1 = createFocusableNode( + this.testFocusableGroup2, + 'testFocusableGroup2.node1', + ); + }); + + teardown(function () { + sharedTestTeardown.call(this); + + // Remove the globally registered listener from FocusManager to avoid state being shared across + // test boundaries. + const eventType = this.globalDocumentEventListenerType; + const eventListener = this.globalDocumentEventListener; + document.removeEventListener(eventType, eventListener); + + const removeFocusIndicators = function (element) { + element.classList.remove('blocklyActiveFocus', 'blocklyPassiveFocus'); + }; + + // Ensure all node CSS styles are reset so that state isn't leaked between tests. + removeFocusIndicators(document.getElementById('testFocusableTree1')); + removeFocusIndicators(document.getElementById('testFocusableTree1.node1')); + removeFocusIndicators( + document.getElementById('testFocusableTree1.node1.child1'), + ); + removeFocusIndicators(document.getElementById('testFocusableTree1.node2')); + removeFocusIndicators(document.getElementById('testFocusableTree2')); + removeFocusIndicators(document.getElementById('testFocusableTree2.node1')); + removeFocusIndicators(document.getElementById('testFocusableGroup1')); + removeFocusIndicators(document.getElementById('testFocusableGroup1.node1')); + removeFocusIndicators( + document.getElementById('testFocusableGroup1.node1.child1'), + ); + removeFocusIndicators(document.getElementById('testFocusableGroup1.node2')); + removeFocusIndicators(document.getElementById('testFocusableGroup2')); + removeFocusIndicators(document.getElementById('testFocusableGroup2.node1')); + }); + + /* Basic lifecycle tests. */ + + suite('registerTree()', function () { + test('once does not throw', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + // The test should pass due to no exception being thrown. + }); + + test('twice for same tree throws error', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + const errorMsgRegex = + /Attempted to re-register already registered tree.+?/; + assert.throws( + () => this.focusManager.registerTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + + test('twice with different trees does not throw', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableGroup1); + + // The test shouldn't throw since two different trees were registered. + }); + + test('register after an unregister does not throw', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + this.focusManager.registerTree(this.testFocusableTree1); + + // The second register should not fail since the tree was previously unregistered. + }); + }); + + suite('unregisterTree()', function () { + test('for not yet registered tree throws', function () { + const errorMsgRegex = /Attempted to unregister not registered tree.+?/; + assert.throws( + () => this.focusManager.unregisterTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + + test('for registered tree does not throw', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Unregistering a registered tree should not fail. + }); + + test('twice for registered tree throws', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const errorMsgRegex = /Attempted to unregister not registered tree.+?/; + assert.throws( + () => this.focusManager.unregisterTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + }); + + suite('isRegistered()', function () { + test('for not registered tree returns false', function () { + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isFalse(isRegistered); + }); + + test('for registered tree returns true', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isTrue(isRegistered); + }); + + test('for unregistered tree returns false', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isFalse(isRegistered); + }); + + test('for re-registered tree returns true', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree1); + + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isTrue(isRegistered); + }); + }); + + suite('getFocusedTree()', function () { + test('by default returns null', function () { + const focusedTree = this.focusManager.getFocusedTree(); + + assert.isNull(focusedTree); + }); + }); + + suite('getFocusedNode()', function () { + test('by default returns null', function () { + const focusedNode = this.focusManager.getFocusedNode(); + + assert.isNull(focusedNode); + }); + }); + + suite('focusTree()', function () { + test('for not registered tree throws', function () { + const errorMsgRegex = /Attempted to focus unregistered tree.+?/; + assert.throws( + () => this.focusManager.focusTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + + test('for unregistered tree throws', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const errorMsgRegex = /Attempted to focus unregistered tree.+?/; + assert.throws( + () => this.focusManager.focusTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + }); + + suite('focusNode()', function () { + test('for not registered node throws', function () { + const errorMsgRegex = /Attempted to focus unregistered node.+?/; + assert.throws( + () => this.focusManager.focusNode(this.testFocusableTree1Node1), + errorMsgRegex, + ); + }); + + test('for unregistered node throws', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const errorMsgRegex = /Attempted to focus unregistered node.+?/; + assert.throws( + () => this.focusManager.focusNode(this.testFocusableTree1Node1), + errorMsgRegex, + ); + }); + }); + + /* Focus tests for HTML trees. */ + + suite('focus*() switching in HTML tree', function () { + suite('getFocusedTree()', function () { + test('registered tree focusTree()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('registered tree focusTree()ed prev node focused returns tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree2); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree2); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('registered root focusNode()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode( + this.testFocusableTree1.getRootFocusableNode(), + ); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered node focusNode()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered subnode focusNode()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1Child1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('registered node focusNode()ed after prev node focus returns same tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered node focusNode()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test("registered tree root focusNode()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode( + this.testFocusableTree2.getRootFocusableNode(), + ); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('unregistered tree focusTree()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with prev node recently focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the most recent tree still exists, it still has focus. + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + }); + suite('getFocusedNode()', function () { + test('registered tree focusTree()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree1.getRootFocusableNode(), + ); + }); + + test('registered tree focusTree()ed prev node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree1); + + // The original node retains focus since the tree already holds focus (per focusTree's + // contract). + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused returns new root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree2); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2.getRootFocusableNode(), + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused returns new root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree2); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2.getRootFocusableNode(), + ); + }); + + test('registered root focusNode()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode( + this.testFocusableTree1.getRootFocusableNode(), + ); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree1.getRootFocusableNode(), + ); + }); + + test('registered node focusNode()ed no prev focus returns node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1, + ); + }); + + test('registered subnode focusNode()ed no prev focus returns subnode', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1Child1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1Child1, + ); + }); + + test('registered node focusNode()ed after prev node focus returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node2, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + + test('registered tree root focusNode()ed after prev node focus diff tree returns new root', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode( + this.testFocusableTree2.getRootFocusableNode(), + ); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2.getRootFocusableNode(), + ); + }); + + test('unregistered tree focusTree()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with prev node recently focused returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the most recent tree still exists, it still has focus. + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + }); + suite('CSS classes', function () { + test('registered tree focusTree()ed no prev focus root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree1); + + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered tree focusTree()ed prev node focused original elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree1); + + // The original node retains active focus since the tree already holds focus (per + // focusTree's contract). + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered tree focusTree()ed diff tree prev focused new root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree2); + + const rootElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused new root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree2); + + const rootElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered root focusNode()ed no prev focus returns root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode( + this.testFocusableTree1.getRootFocusableNode(), + ); + + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focusNode()ed no prev focus node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focusNode()ed after prev node focus same tree old node elem has no focus property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focusNode()ed after prev node focus same tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + const newNodeElem = this.testFocusableTree1Node2.getFocusableElement(); + assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(newNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree old node elem has passive property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.include( + Array.from(prevNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const newNodeElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(newNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered tree root focusNode()ed after prev node focus diff tree new root has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode( + this.testFocusableTree2.getRootFocusableNode(), + ); + + const rootElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focusTree()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focusNode()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focusNode()ed with prev node prior removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. However, the old node + // should still have passive indication. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.include( + Array.from(otherNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude( + Array.from(otherNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focusNode()ed with prev node recently removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. However, the new node + // should still have active indication. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.include( + Array.from(otherNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(otherNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('focusNode() multiple nodes in same tree with switches ensure passive focus has gone', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + // When switching back to the first tree, ensure the original passive node is no longer + // passive now that the new node is active. + const node1 = this.testFocusableTree1Node1.getFocusableElement(); + const node2 = this.testFocusableTree1Node2.getFocusableElement(); + assert.notInclude(Array.from(node1.classList), 'blocklyPassiveFocus'); + assert.notInclude(Array.from(node2.classList), 'blocklyPassiveFocus'); + }); + + test('registered tree focusTree()ed other tree node passively focused tree node now has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusTree(this.testFocusableTree1); + + // The original node in the tree should be moved from passive to active focus per the + // contract of focusTree). Also, the root of the tree should have no focus indication. + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + }); + }); + + suite('DOM focus() switching in HTML tree', function () { + suite('getFocusedTree()', function () { + test('registered root focus()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered node focus()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered subnode focus()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1.child1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('registered node focus()ed after prev node focus returns same tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered node focus()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test("registered tree root focus()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test("non-registered node subelement focus()ed returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document + .getElementById('testFocusableTree1.node2.unregisteredChild1') + .focus(); + + // The tree of the unregistered child element should take focus. + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('non-registered tree focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableTree3').focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('non-registered tree node focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('non-registered tree node focus()ed after registered node focused returns original tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree2.node1').focus(); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with prev node recently focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the most recent tree still exists, it still has focus. + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering still returns old tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + // Attempting to focus a now removed tree should have no effect. + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + }); + suite('getFocusedNode()', function () { + test('registered root focus()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree1.getRootFocusableNode(), + ); + }); + + test('registered node focus()ed no prev focus returns node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1, + ); + }); + + test('registered subnode focus()ed no prev focus returns subnode', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1.child1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1Child1, + ); + }); + + test('registered node focus()ed after prev node focus returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node2, + ); + }); + + test('registered node focus()ed after prev node focus diff tree returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + + test('registered tree root focus()ed after prev node focus diff tree returns new root', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2.getRootFocusableNode(), + ); + }); + + test('non-registered node subelement focus()ed returns nearest node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document + .getElementById('testFocusableTree1.node2.unregisteredChild1') + .focus(); + + // The nearest node of the unregistered child element should take focus. + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node2, + ); + }); + + test('non-registered tree focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableTree3').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('non-registered tree node focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('non-registered tree node focus()ed after registered node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1, + ); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree2.node1').focus(); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with prev node recently focused returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the most recent tree still exists, it still has focus. + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering still returns old node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + // Attempting to focus a now removed tree should have no effect. + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + }); + suite('CSS classes', function () { + test('registered root focus()ed no prev focus returns root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1').focus(); + + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focus()ed no prev focus node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focus()ed after prev node focus same tree old node elem has no focus property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focus()ed after prev node focus same tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + const newNodeElem = this.testFocusableTree1Node2.getFocusableElement(); + assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(newNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focus()ed after prev node focus diff tree old node elem has passive property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.include( + Array.from(prevNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focus()ed after prev node focus diff tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + const newNodeElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(newNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered tree root focus()ed after prev node focus diff tree new root has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2').focus(); + + const rootElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('non-registered node subelement focus()ed nearest node has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document + .getElementById('testFocusableTree1.node2.unregisteredChild1') + .focus(); + + // The nearest node of the unregistered child element should be actively focused. + const nodeElem = this.testFocusableTree1Node2.getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('non-registered tree focus()ed has no focus', function () { + document.getElementById('testUnregisteredFocusableTree3').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + + const rootElem = document.getElementById( + 'testUnregisteredFocusableTree3', + ); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('non-registered tree node focus()ed has no focus', function () { + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + + const nodeElem = document.getElementById( + 'testUnregisteredFocusableTree3.node1', + ); + assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('non-registered tree node focus()ed after registered node focused original node has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + // The original node should be unchanged, and the unregistered node should not have any + // focus indicators. + const nodeElem = document.getElementById('testFocusableTree1.node1'); + const attemptedNewNodeElem = document.getElementById( + 'testUnregisteredFocusableTree3.node1', + ); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude( + Array.from(attemptedNewNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(attemptedNewNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focus()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focus()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focus()ed with prev node prior removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree2.node1').focus(); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. However, the old node + // should still have passive indication. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.include( + Array.from(otherNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude( + Array.from(otherNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focus()ed with prev node recently removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. However, the new node + // should still have active indication. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.include( + Array.from(otherNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(otherNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering does not change indicators', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + // Attempting to focus a now removed tree should have no effect. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.include( + Array.from(otherNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(otherNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('focus() multiple nodes in same tree with switches ensure passive focus has gone', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + // When switching back to the first tree, ensure the original passive node is no longer + // passive now that the new node is active. + const node1 = this.testFocusableTree1Node1.getFocusableElement(); + const node2 = this.testFocusableTree1Node2.getFocusableElement(); + assert.notInclude(Array.from(node1.classList), 'blocklyPassiveFocus'); + assert.notInclude(Array.from(node2.classList), 'blocklyPassiveFocus'); + }); + + test('registered tree focus()ed other tree node passively focused tree root now has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableTree1').focus(); + + // This differs from the behavior of focusTree() since directly focusing a tree's root will + // coerce it to now have focus. + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableTree1.node1').focus(); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + }); + }); + + /* Focus tests for SVG trees. */ + + suite('focus*() switching in SVG tree', function () { + suite('getFocusedTree()', function () { + test('registered tree focusTree()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('registered tree focusTree()ed prev node focused returns tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('registered root focusNode()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode( + this.testFocusableGroup1.getRootFocusableNode(), + ); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered node focusNode()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered subnode focusNode()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1Child1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('registered node focusNode()ed after prev node focus returns same tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered node focusNode()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test("registered tree root focusNode()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode( + this.testFocusableGroup2.getRootFocusableNode(), + ); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('unregistered tree focusTree()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with prev node recently focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the most recent tree still exists, it still has focus. + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + }); + suite('getFocusedNode()', function () { + test('registered tree focusTree()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1.getRootFocusableNode(), + ); + }); + + test('registered tree focusTree()ed prev node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + // The original node retains focus since the tree already holds focus (per focusTree's + // contract). + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused returns new root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2.getRootFocusableNode(), + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused returns new root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2.getRootFocusableNode(), + ); + }); + + test('registered root focusNode()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode( + this.testFocusableGroup1.getRootFocusableNode(), + ); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1.getRootFocusableNode(), + ); + }); + + test('registered node focusNode()ed no prev focus returns node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1, + ); + }); + + test('registered subnode focusNode()ed no prev focus returns subnode', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1Child1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1Child1, + ); + }); + + test('registered node focusNode()ed after prev node focus returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node2, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + + test('registered tree root focusNode()ed after prev node focus diff tree returns new root', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode( + this.testFocusableGroup2.getRootFocusableNode(), + ); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2.getRootFocusableNode(), + ); + }); + + test('unregistered tree focusTree()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with prev node recently focused returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the most recent tree still exists, it still has focus. + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + }); + suite('CSS classes', function () { + test('registered tree focusTree()ed no prev focus root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered tree focusTree()ed prev node focused original elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + // The original node retains active focus since the tree already holds focus (per + // focusTree's contract). + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered tree focusTree()ed diff tree prev focused new root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused new root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered root focusNode()ed no prev focus returns root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode( + this.testFocusableGroup1.getRootFocusableNode(), + ); + + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focusNode()ed no prev focus node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focusNode()ed after prev node focus same tree old node elem has no focus property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + const prevNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focusNode()ed after prev node focus same tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + const newNodeElem = this.testFocusableGroup1Node2.getFocusableElement(); + assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(newNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree old node elem has passive property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const prevNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.include( + Array.from(prevNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const newNodeElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(newNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered tree root focusNode()ed after prev node focus diff tree new root has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode( + this.testFocusableGroup2.getRootFocusableNode(), + ); + + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focusTree()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focusNode()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focusNode()ed with prev node prior removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. However, the old node + // should still have passive indication. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.include( + Array.from(otherNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude( + Array.from(otherNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focusNode()ed with prev node recently removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. However, the new node + // should still have active indication. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.include( + Array.from(otherNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(otherNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('focusNode() multiple nodes in same tree with switches ensure passive focus has gone', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + // When switching back to the first tree, ensure the original passive node is no longer + // passive now that the new node is active. + const node1 = this.testFocusableGroup1Node1.getFocusableElement(); + const node2 = this.testFocusableGroup1Node2.getFocusableElement(); + assert.notInclude(Array.from(node1.classList), 'blocklyPassiveFocus'); + assert.notInclude(Array.from(node2.classList), 'blocklyPassiveFocus'); + }); + + test('registered tree focusTree()ed other tree node passively focused tree node now has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + // The original node in the tree should be moved from passive to active focus per the + // contract of focusTree). Also, the root of the tree should have no focus indication. + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + }); + }); + + suite('DOM focus() switching in SVG tree', function () { + suite('getFocusedTree()', function () { + test('registered root focus()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered node focus()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered subnode focus()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1.child1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('registered node focus()ed after prev node focus returns same tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered node focus()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test("registered tree root focus()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test("non-registered node subelement focus()ed returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document + .getElementById('testFocusableGroup1.node2.unregisteredChild1') + .focus(); + + // The tree of the unregistered child element should take focus. + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('non-registered tree focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableGroup3').focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('non-registered tree node focus()ed returns null', function () { + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('non-registered tree node focus()ed after registered node focused returns original tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').focus(); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with prev node recently focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the most recent tree still exists, it still has focus. + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering still returns old tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + // Attempting to focus a now removed tree should have no effect. + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + }); + suite('getFocusedNode()', function () { + test('registered root focus()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1.getRootFocusableNode(), + ); + }); + + test('registered node focus()ed no prev focus returns node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1, + ); + }); + + test('registered subnode focus()ed no prev focus returns subnode', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1.child1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1Child1, + ); + }); + + test('registered node focus()ed after prev node focus returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node2, + ); + }); + + test('registered node focus()ed after prev node focus diff tree returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + + test('registered tree root focus()ed after prev node focus diff tree returns new root', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2.getRootFocusableNode(), + ); + }); + + test('non-registered node subelement focus()ed returns nearest node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document + .getElementById('testFocusableGroup1.node2.unregisteredChild1') + .focus(); + + // The nearest node of the unregistered child element should take focus. + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node2, + ); + }); + + test('non-registered tree focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableGroup3').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('non-registered tree node focus()ed returns null', function () { + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('non-registered tree node focus()ed after registered node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1, + ); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').focus(); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with prev node recently focused returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the most recent tree still exists, it still has focus. + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering still returns old node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + // Attempting to focus a now removed tree should have no effect. + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + }); + suite('CSS classes', function () { + test('registered root focus()ed no prev focus returns root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1').focus(); + + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focus()ed no prev focus node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focus()ed after prev node focus same tree old node elem has no focus property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + const prevNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focus()ed after prev node focus same tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + const newNodeElem = this.testFocusableGroup1Node2.getFocusableElement(); + assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(newNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focus()ed after prev node focus diff tree old node elem has passive property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const prevNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.include( + Array.from(prevNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered node focus()ed after prev node focus diff tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const newNodeElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(newNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('registered tree root focus()ed after prev node focus diff tree new root has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2').focus(); + + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('non-registered node subelement focus()ed nearest node has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document + .getElementById('testFocusableGroup1.node2.unregisteredChild1') + .focus(); + + // The nearest node of the unregistered child element should be actively focused. + const nodeElem = this.testFocusableGroup1Node2.getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('non-registered tree focus()ed has no focus', function () { + document.getElementById('testUnregisteredFocusableGroup3').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + + const rootElem = document.getElementById( + 'testUnregisteredFocusableGroup3', + ); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('non-registered tree node focus()ed has no focus', function () { + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + + const nodeElem = document.getElementById( + 'testUnregisteredFocusableGroup3.node1', + ); + assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('non-registered tree node focus()ed after registered node focused original node has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + // The original node should be unchanged, and the unregistered node should not have any + // focus indicators. + const nodeElem = document.getElementById('testFocusableGroup1.node1'); + const attemptedNewNodeElem = document.getElementById( + 'testUnregisteredFocusableGroup3.node1', + ); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude( + Array.from(attemptedNewNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(attemptedNewNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focus()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focus()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focus()ed with prev node prior removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').focus(); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. However, the old node + // should still have passive indication. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.include( + Array.from(otherNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude( + Array.from(otherNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focus()ed with prev node recently removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. However, the new node + // should still have active indication. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.include( + Array.from(otherNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(otherNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering does not change indicators', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + // Attempting to focus a now removed tree should have no effect. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.include( + Array.from(otherNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(otherNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(removedNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('focus() multiple nodes in same tree with switches ensure passive focus has gone', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + // When switching back to the first tree, ensure the original passive node is no longer + // passive now that the new node is active. + const node1 = this.testFocusableGroup1Node1.getFocusableElement(); + const node2 = this.testFocusableGroup1Node2.getFocusableElement(); + assert.notInclude(Array.from(node1.classList), 'blocklyPassiveFocus'); + assert.notInclude(Array.from(node2.classList), 'blocklyPassiveFocus'); + }); + + test('registered tree focus()ed other tree node passively focused tree root now has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableGroup1').focus(); + + // This differs from the behavior of focusTree() since directly focusing a tree's root will + // coerce it to now have focus. + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableGroup1.node1').focus(); + + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + }); + }); + + /* Combined HTML/SVG tree focus tests. */ + + suite('HTML/SVG focus tree switching', function () { + suite('Focus HTML tree then SVG tree', function () { + test('HTML focusTree()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const prevElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('HTML focusTree()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const prevElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('HTML focusTree()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const prevElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('HTML focusNode()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('HTML focusNode()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('HTML focusNode()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('HTML DOM focus()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('HTML DOM focus()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('HTML DOM focus()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + }); + suite('Focus SVG tree then HTML tree', function () { + test('SVG focusTree()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup2); + + this.focusManager.focusTree(this.testFocusableTree2); + + const prevElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('SVG focusTree()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup2); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const prevElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('SVG focusTree()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup2); + + document.getElementById('testFocusableTree2.node1').focus(); + + const prevElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('SVG focusNode()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusTree(this.testFocusableTree2); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('SVG focusNode()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('SVG focusNode()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + document.getElementById('testFocusableTree2.node1').focus(); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('SVG DOM focus()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.focusTree(this.testFocusableTree2); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('SVG DOM focus()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + + test('SVG DOM focus()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(currElem.classList), + 'blocklyPassiveFocus', + ); + assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); + assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + }); + }); + }); + + /* Ephemeral focus tests. */ + + suite('takeEphemeralFocus()', function () { + function classListOf(node) { + return Array.from(node.getFocusableElement().classList); + } + + test('with no focused node does not change states', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + // Taking focus without an existing node having focus should change no focus indicators. + const activeElems = Array.from( + document.querySelectorAll('.blocklyActiveFocus'), + ); + const passiveElems = Array.from( + document.querySelectorAll('.blocklyPassiveFocus'), + ); + assert.isEmpty(activeElems); + assert.isEmpty(passiveElems); + }); + + test('with focused node changes focused node to passive', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + // Taking focus without an existing node having focus should change no focus indicators. + const activeElems = Array.from( + document.querySelectorAll('.blocklyActiveFocus'), + ); + const passiveElems = Array.from( + document.querySelectorAll('.blocklyPassiveFocus'), + ); + assert.isEmpty(activeElems); + assert.equal(passiveElems.length, 1); + assert.include( + classListOf(this.testFocusableTree2Node1), + 'blocklyPassiveFocus', + ); + }); + + test('focuses provided HTML element', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + assert.strictEqual(document.activeElement, ephemeralElement); + }); + + test('focuses provided SVG element', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + assert.strictEqual(document.activeElement, ephemeralElement); + }); + + test('twice for without finishing previous throws error', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + const ephemeralGroupElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const ephemeralDivElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralGroupElement); + + const errorMsgRegex = + /Attempted to take ephemeral focus when it's already held+?/; + assert.throws( + () => this.focusManager.takeEphemeralFocus(ephemeralDivElement), + errorMsgRegex, + ); + }); + + test('then focusTree() changes getFocusedTree() but not active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + const activeElems = Array.from( + document.querySelectorAll('.blocklyActiveFocus'), + ); + assert.isEmpty(activeElems); + }); + + test('then focusNode() changes getFocusedNode() but not active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + const activeElems = Array.from( + document.querySelectorAll('.blocklyActiveFocus'), + ); + assert.isEmpty(activeElems); + }); + + test('then DOM refocus changes getFocusedNode() but not active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + // Force focus to change via the DOM. + document.getElementById('testFocusableGroup2.node1').focus(); + + // The focus() state change will affect getFocusedNode() but it will not cause the node to now + // be active. + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + const activeElems = Array.from( + document.querySelectorAll('.blocklyActiveFocus'), + ); + assert.isEmpty(activeElems); + }); + + test('then finish ephemeral callback with no node does not change indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + finishFocusCallback(); + + // Finishing ephemeral focus without a previously focused node should not change indicators. + const activeElems = Array.from( + document.querySelectorAll('.blocklyActiveFocus'), + ); + const passiveElems = Array.from( + document.querySelectorAll('.blocklyPassiveFocus'), + ); + assert.isEmpty(activeElems); + assert.isEmpty(passiveElems); + }); + + test('again after finishing previous empheral focus should focus new element', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + const ephemeralGroupElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const ephemeralDivElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + const finishFocusCallback = this.focusManager.takeEphemeralFocus( + ephemeralGroupElement, + ); + + finishFocusCallback(); + this.focusManager.takeEphemeralFocus(ephemeralDivElement); + + // An exception should not be thrown and the new element should receive focus. + assert.strictEqual(document.activeElement, ephemeralDivElement); + }); + + test('calling ephemeral callback twice throws error', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + finishFocusCallback(); + + const errorMsgRegex = + /Attempted to finish ephemeral focus twice for element+?/; + assert.throws(() => finishFocusCallback(), errorMsgRegex); + }); + + test('then finish ephemeral callback should restore focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + finishFocusCallback(); + + // The original focused node should be restored. + const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); + const activeElems = Array.from( + document.querySelectorAll('.blocklyActiveFocus'), + ); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.equal(activeElems.length, 1); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.strictEqual(document.activeElement, nodeElem); + }); + + test('then focusTree() and finish ephemeral callback correctly sets new active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + this.focusManager.focusTree(this.testFocusableGroup2); + finishFocusCallback(); + + // The tree's root should now be the active element since focus changed between the start and + // end of the ephemeral flow. + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + const activeElems = Array.from( + document.querySelectorAll('.blocklyActiveFocus'), + ); + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + assert.equal(activeElems.length, 1); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.strictEqual(document.activeElement, rootElem); + }); + + test('then focusNode() and finish ephemeral callback correctly sets new active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + finishFocusCallback(); + + // The tree's root should now be the active element since focus changed between the start and + // end of the ephemeral flow. + const nodeElem = this.testFocusableGroup2Node1.getFocusableElement(); + const activeElems = Array.from( + document.querySelectorAll('.blocklyActiveFocus'), + ); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.equal(activeElems.length, 1); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.strictEqual(document.activeElement, nodeElem); + }); + + test('then DOM focus change and finish ephemeral callback correctly sets new active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + document.getElementById('testFocusableGroup2.node1').focus(); + finishFocusCallback(); + + // The tree's root should now be the active element since focus changed between the start and + // end of the ephemeral flow. + const nodeElem = this.testFocusableGroup2Node1.getFocusableElement(); + const activeElems = Array.from( + document.querySelectorAll('.blocklyActiveFocus'), + ); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.equal(activeElems.length, 1); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.strictEqual(document.activeElement, nodeElem); + }); + }); +}); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 008d1f1b153..2eb42869aad 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -13,11 +13,82 @@ visibility: hidden; width: 1000px; } + + .blocklyActiveFocus { + outline-color: #0f0; + outline-width: 2px; + } + .blocklyPassiveFocus { + outline-color: #00f; + outline-width: 1.5px; + } + div.blocklyActiveFocus { + color: #0f0; + } + div.blocklyPassiveFocus { + color: #00f; + } + g.blocklyActiveFocus { + fill: #0f0; + } + g.blocklyPassiveFocus { + fill: #00f; + }
+
+
+ Tree 1 node 1 +
Tree 1 node 1 child 1
+
+
+ Tree 1 node 2 +
Tree 1 node 2 child 2 (unregistered)
+
+
+
+
Tree 2 node 1
+
+
+
Tree 3 node 1 (unregistered)
+
+
+ + + + + Group 1 node 1 + + + Tree 1 node 1 child 1 + + + + + Group 1 node 2 + + + Tree 1 node 2 child 2 (unregistered) + + + + + + + Group 2 node 1 + + + + + + Tree 3 node 1 (unregistered) + + + + @@ -90,6 +161,8 @@ import './field_textinput_test.js'; import './field_variable_test.js'; import './flyout_test.js'; + import './focus_manager_test.js'; + // import './test_event_reduction.js'; import './generator_test.js'; import './gesture_test.js'; import './icon_test.js'; From 63a4df65338c6582615d1518a0d2929152840383 Mon Sep 17 00:00:00 2001 From: zhiyan wang Date: Tue, 25 Mar 2025 04:18:30 +0800 Subject: [PATCH 108/222] fix: fix bug that modalInputs option is not working in toolbox area (#8817) --- core/toolbox/toolbox.ts | 1 + core/trashcan.ts | 1 + core/workspace_svg.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 12037839399..556bc89e685 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -333,6 +333,7 @@ export class Toolbox 'horizontalLayout': workspace.horizontalLayout, 'renderer': workspace.options.renderer, 'rendererOverrides': workspace.options.rendererOverrides, + 'modalInputs': workspace.options.modalInputs, 'move': { 'scrollbars': true, }, diff --git a/core/trashcan.ts b/core/trashcan.ts index 05ae9fbf270..fdff7c50b48 100644 --- a/core/trashcan.ts +++ b/core/trashcan.ts @@ -110,6 +110,7 @@ export class Trashcan 'oneBasedIndex': this.workspace.options.oneBasedIndex, 'renderer': this.workspace.options.renderer, 'rendererOverrides': this.workspace.options.rendererOverrides, + 'modalInputs': this.workspace.options.modalInputs, 'move': { 'scrollbars': true, }, diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 6acd31c9c7f..94254a1ba20 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -953,6 +953,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { 'horizontalLayout': this.horizontalLayout, 'renderer': this.options.renderer, 'rendererOverrides': this.options.rendererOverrides, + 'modalInputs': this.options.modalInputs, 'move': { 'scrollbars': true, }, From 74ad061ef4eec885f1c65a125b9e9338f2cb5b79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:46:15 -0700 Subject: [PATCH 109/222] chore(deps): bump chai from 5.1.2 to 5.2.0 (#8807) Bumps [chai](https://github.com/chaijs/chai) from 5.1.2 to 5.2.0. - [Release notes](https://github.com/chaijs/chai/releases) - [Changelog](https://github.com/chaijs/chai/blob/main/History.md) - [Commits](https://github.com/chaijs/chai/compare/v5.1.2...v5.2.0) --- updated-dependencies: - dependency-name: chai dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae683ebfc6d..42718bd938e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2664,10 +2664,11 @@ } }, "node_modules/chai": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", - "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, + "license": "MIT", "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", From 516e3af936b2ba0cba511dc22378d4361be96d80 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Mar 2025 21:57:30 +0000 Subject: [PATCH 110/222] feat: finish core impl + tests This adds new tests for the FocusableTreeTraverser and fixes a number of issues with the original implementation (one of which required two new API methods to be added to IFocusableTree). More tests have also been added for FocusManager, and defocusing tracked nodes/trees has been fully implemented in FocusManager. --- core/focus_manager.ts | 12 +- core/interfaces/i_focusable_tree.ts | 28 +- core/utils/focusable_tree_traverser.ts | 74 +- tests/mocha/focus_manager_test.js | 833 +++++++++++++++++-- tests/mocha/focusable_tree_traverser_test.js | 480 +++++++++++ tests/mocha/index.html | 62 +- 6 files changed, 1399 insertions(+), 90 deletions(-) create mode 100644 tests/mocha/focusable_tree_traverser_test.js diff --git a/core/focus_manager.ts b/core/focus_manager.ts index 5e6e0af48ab..d79db4b4bcd 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -62,7 +62,7 @@ export class FocusManager { if (newNode) { this.focusNode(newNode); } else { - // TODO: Set previous to passive if all trees are losing active focus. + this.defocusCurrentFocusedNode(); } }); } @@ -259,6 +259,16 @@ export class FocusManager { }; } + private defocusCurrentFocusedNode(): void { + // The current node will likely be defocused while ephemeral focus is held, + // but internal manager state shouldn't change since the node should be + // restored upon exiting ephemeral focus mode. + if (this.focusedNode && !this.currentlyHoldsEphemeralFocus) { + this.setNodeToPassive(this.focusedNode); + this.focusedNode = null; + } + } + private setNodeToActive(node: IFocusableNode): void { const element = node.getFocusableElement(); dom.addClass(element, 'blocklyActiveFocus'); diff --git a/core/interfaces/i_focusable_tree.ts b/core/interfaces/i_focusable_tree.ts index 1a8ccf82b4a..9cedba732fa 100644 --- a/core/interfaces/i_focusable_tree.ts +++ b/core/interfaces/i_focusable_tree.ts @@ -40,6 +40,28 @@ export interface IFocusableTree { */ getRootFocusableNode(): IFocusableNode; + /** + * Returns all directly nested trees under this tree. + * + * Note that the returned list of trees doesn't need to be stable, however all + * returned trees *do* need to be registered with FocusManager. Additionally, + * this must return actual nested trees as omitting a nested tree will affect + * how focus changes map to a specific node and its tree, potentially leading + * to user confusion. + */ + getNestedTrees(): Array; + + /** + * Returns the IFocusableNode corresponding to the specified element ID, or + * null if there's no exact node within this tree with that ID or if the ID + * corresponds to the root of the tree. + * + * This will never match against nested trees. + * + * @param id The ID of the node's focusable HTMLElement or SVGElement. + */ + lookUpFocusableNode(id: string): IFocusableNode | null; + /** * Returns the IFocusableNode corresponding to the select element, or null if * the element does not have such a node. @@ -47,7 +69,11 @@ export interface IFocusableTree { * The provided element must have a non-null ID that conforms to the contract * mentioned in IFocusableNode. * - * This function may match against the root node of the tree. + * This function may match against the root node of the tree. It will also map + * against the nearest node to the provided element if the element does not + * have an exact matching corresponding node. This function filters out + * matches against nested trees, so long as they are represented in the return + * value of getNestedTrees. */ findFocusableNodeFor( element: HTMLElement | SVGElement, diff --git a/core/utils/focusable_tree_traverser.ts b/core/utils/focusable_tree_traverser.ts index b7465e884b4..eb6de1e0596 100644 --- a/core/utils/focusable_tree_traverser.ts +++ b/core/utils/focusable_tree_traverser.ts @@ -6,6 +6,7 @@ import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; +import * as dom from '../utils/dom.js'; /** * A helper utility for IFocusableTree implementations to aid with common @@ -24,20 +25,29 @@ export class FocusableTreeTraverser { */ static findFocusedNode(tree: IFocusableTree): IFocusableNode | null { const root = tree.getRootFocusableNode().getFocusableElement(); - const activeElem = root.querySelector('.blocklyActiveFocus'); + if ( + dom.hasClass(root, 'blocklyActiveFocus') || + dom.hasClass(root, 'blocklyPassiveFocus') + ) { + // The root has focus. + return tree.getRootFocusableNode(); + } + + const activeEl = root.querySelector('.blocklyActiveFocus'); let active: IFocusableNode | null = null; - if (activeElem instanceof HTMLElement || activeElem instanceof SVGElement) { - active = tree.findFocusableNodeFor(activeElem); + if (activeEl instanceof HTMLElement || activeEl instanceof SVGElement) { + active = tree.findFocusableNodeFor(activeEl); } - const passiveElems = Array.from( - root.querySelectorAll('.blocklyPassiveFocus'), - ); - const passive = passiveElems.map((elem) => { - if (elem instanceof HTMLElement || elem instanceof SVGElement) { - return tree.findFocusableNodeFor(elem); - } else return null; - }); - return active || passive.find((node) => !!node) || null; + + // At most there should be one passive indicator per tree (not considering + // subtrees). + const passiveEl = root.querySelector('.blocklyPassiveFocus'); + let passive: IFocusableNode | null = null; + if (passiveEl instanceof HTMLElement || passiveEl instanceof SVGElement) { + passive = tree.findFocusableNodeFor(passiveEl); + } + + return active ?? passive; } /** @@ -47,38 +57,42 @@ export class FocusableTreeTraverser { * * If the tree contains another nested IFocusableTree, the nested tree may be * traversed but its nodes will never be returned here per the contract of - * findChildById. - * - * findChildById is a provided callback that takes an element ID and maps it - * back to the corresponding IFocusableNode within the provided - * IFocusableTree. These IDs will match the contract specified in the - * documentation for IFocusableNode. This function must not return any node - * that doesn't directly belong to the node's nearest parent tree. + * IFocusableTree.lookUpFocusableNode. * * @param element The HTML or SVG element being sought. * @param tree The tree under which the provided element may be a descendant. - * @param findChildById The ID->IFocusableNode mapping callback that must - * follow the contract mentioned above. * @returns The matching IFocusableNode, or null if there is no match. */ static findFocusableNodeFor( element: HTMLElement | SVGElement, tree: IFocusableTree, - findChildById: (id: string) => IFocusableNode | null, ): IFocusableNode | null { + // First, match against subtrees. + const subTreeMatches = tree + .getNestedTrees() + .map((tree) => tree.findFocusableNodeFor(element)); + if (subTreeMatches.findIndex((match) => !!match) !== -1) { + // At least one subtree has a match for the element so it cannot be part + // of the outer tree. + return null; + } + + // Second, check against the tree's root. if (element === tree.getRootFocusableNode().getFocusableElement()) { return tree.getRootFocusableNode(); } - const matchedChildNode = findChildById(element.id); + + // Third, check if the element has a node. + const matchedChildNode = tree.lookUpFocusableNode(element.id) ?? null; + if (matchedChildNode) return matchedChildNode; + + // Fourth, recurse up to find the nearest tree/node if it's possible. const elementParent = element.parentElement; if (!matchedChildNode && elementParent) { - // Recurse up to find the nearest tree/node. - return FocusableTreeTraverser.findFocusableNodeFor( - elementParent, - tree, - findChildById, - ); + return FocusableTreeTraverser.findFocusableNodeFor(elementParent, tree); } - return matchedChildNode; + + // Otherwise, there's no matching node. + return null; } } diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js index 86a19fd1857..e18dbc79e67 100644 --- a/tests/mocha/focus_manager_test.js +++ b/tests/mocha/focus_manager_test.js @@ -1,10 +1,13 @@ /** * @license - * Copyright 2020 Google LLC + * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import {FocusManager} from '../../build/src/core/focus_manager.js'; +import { + FocusManager, + getFocusManager, +} from '../../build/src/core/focus_manager.js'; import {FocusableTreeTraverser} from '../../build/src/core/utils/focusable_tree_traverser.js'; import {assert} from '../../node_modules/chai/chai.js'; import { @@ -33,7 +36,7 @@ suite('FocusManager', function () { return tree; }; }; - const FocusableTreeImpl = function (rootElement) { + const FocusableTreeImpl = function (rootElement, nestedTrees) { this.idToNodeMap = {}; this.addNode = function (element) { @@ -50,19 +53,26 @@ suite('FocusManager', function () { return this.rootNode; }; + this.getNestedTrees = function () { + return nestedTrees; + }; + + this.lookUpFocusableNode = function (id) { + return this.idToNodeMap[id]; + }; + this.findFocusableNodeFor = function (element) { - return FocusableTreeTraverser.findFocusableNodeFor( - element, - this, - (id) => this.idToNodeMap[id], - ); + return FocusableTreeTraverser.findFocusableNodeFor(element, this); }; this.rootNode = this.addNode(rootElement); }; - const createFocusableTree = function (rootElementId) { - return new FocusableTreeImpl(document.getElementById(rootElementId)); + const createFocusableTree = function (rootElementId, nestedTrees) { + return new FocusableTreeImpl( + document.getElementById(rootElementId), + nestedTrees || [], + ); }; const createFocusableNode = function (tree, elementId) { return tree.addNode(document.getElementById(elementId)); @@ -81,11 +91,29 @@ suite('FocusManager', function () { this.testFocusableTree1, 'testFocusableTree1.node2', ); - this.testFocusableTree2 = createFocusableTree('testFocusableTree2'); + this.testFocusableNestedTree4 = createFocusableTree( + 'testFocusableNestedTree4', + ); + this.testFocusableNestedTree4Node1 = createFocusableNode( + this.testFocusableNestedTree4, + 'testFocusableNestedTree4.node1', + ); + this.testFocusableNestedTree5 = createFocusableTree( + 'testFocusableNestedTree5', + ); + this.testFocusableNestedTree5Node1 = createFocusableNode( + this.testFocusableNestedTree5, + 'testFocusableNestedTree5.node1', + ); + this.testFocusableTree2 = createFocusableTree('testFocusableTree2', [ + this.testFocusableNestedTree4, + this.testFocusableNestedTree5, + ]); this.testFocusableTree2Node1 = createFocusableNode( this.testFocusableTree2, 'testFocusableTree2.node1', ); + this.testFocusableGroup1 = createFocusableTree('testFocusableGroup1'); this.testFocusableGroup1Node1 = createFocusableNode( this.testFocusableGroup1, @@ -99,7 +127,16 @@ suite('FocusManager', function () { this.testFocusableGroup1, 'testFocusableGroup1.node2', ); - this.testFocusableGroup2 = createFocusableTree('testFocusableGroup2'); + this.testFocusableNestedGroup4 = createFocusableTree( + 'testFocusableNestedGroup4', + ); + this.testFocusableNestedGroup4Node1 = createFocusableNode( + this.testFocusableNestedGroup4, + 'testFocusableNestedGroup4.node1', + ); + this.testFocusableGroup2 = createFocusableTree('testFocusableGroup2', [ + this.testFocusableNestedGroup4, + ]); this.testFocusableGroup2Node1 = createFocusableNode( this.testFocusableGroup2, 'testFocusableGroup2.node1', @@ -128,6 +165,10 @@ suite('FocusManager', function () { removeFocusIndicators(document.getElementById('testFocusableTree1.node2')); removeFocusIndicators(document.getElementById('testFocusableTree2')); removeFocusIndicators(document.getElementById('testFocusableTree2.node1')); + removeFocusIndicators(document.getElementById('testFocusableNestedTree4')); + removeFocusIndicators( + document.getElementById('testFocusableNestedTree4.node1'), + ); removeFocusIndicators(document.getElementById('testFocusableGroup1')); removeFocusIndicators(document.getElementById('testFocusableGroup1.node1')); removeFocusIndicators( @@ -136,6 +177,13 @@ suite('FocusManager', function () { removeFocusIndicators(document.getElementById('testFocusableGroup1.node2')); removeFocusIndicators(document.getElementById('testFocusableGroup2')); removeFocusIndicators(document.getElementById('testFocusableGroup2.node1')); + removeFocusIndicators(document.getElementById('testFocusableNestedGroup4')); + removeFocusIndicators( + document.getElementById('testFocusableNestedGroup4.node1'), + ); + + // Reset the current active element. + document.body.focus(); }); /* Basic lifecycle tests. */ @@ -303,6 +351,43 @@ suite('FocusManager', function () { errorMsgRegex, ); }); + + test('focuses element', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.strictEqual(document.activeElement, nodeElem); + }); + + test('fires focusin event', function () { + let focusCount = 0; + const focusListener = () => focusCount++; + document.addEventListener('focusin', focusListener); + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + document.removeEventListener('focusin', focusListener); + + // There should be exactly 1 focus event fired from focusNode(). + assert.equal(focusCount, 1); + }); + }); + + suite('getFocusManager()', function () { + test('returns non-null manager', function () { + const manager = getFocusManager(); + + assert.isNotNull(manager); + }); + + test('returns the exact same instance in subsequent calls', function () { + const manager1 = getFocusManager(); + const manager2 = getFocusManager(); + + assert.strictEqual(manager2, manager1); + }); }); /* Focus tests for HTML trees. */ @@ -477,6 +562,43 @@ suite('FocusManager', function () { this.testFocusableTree2, ); }); + + test('nested tree focusTree()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusTree(this.testFocusableNestedTree4); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); }); suite('getFocusedNode()', function () { test('registered tree focusTree()ed no prev focus returns root node', function () { @@ -649,6 +771,43 @@ suite('FocusManager', function () { this.testFocusableTree2Node1, ); }); + + test('nested tree focusTree()ed with no prev focus returns nested root', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusTree(this.testFocusableNestedTree4); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4.getRootFocusableNode(), + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + }); }); suite('CSS classes', function () { test('registered tree focusTree()ed no prev focus root elem has active property', function () { @@ -990,6 +1149,65 @@ suite('FocusManager', function () { 'blocklyPassiveFocus', ); }); + + test('nested tree focusTree()ed with no prev root has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusTree(this.testFocusableNestedTree4); + + const rootElem = this.testFocusableNestedTree4 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('nested tree node focusNode()ed with no prev focus node has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + const nodeElem = + this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('nested tree node focusNode()ed after parent focused prev has passive node has active', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + const prevNodeElem = this.testFocusableTree2Node1.getFocusableElement(); + const currNodeElem = + this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.include( + Array.from(prevNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.include( + Array.from(currNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(currNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); }); }); @@ -1092,12 +1310,21 @@ suite('FocusManager', function () { assert.isNull(this.focusManager.getFocusedTree()); }); - test('non-registered tree node focus()ed after registered node focused returns original tree', function () { + test('non-registered tree node focus()ed after registered node focused returns null', function () { this.focusManager.registerTree(this.testFocusableTree1); document.getElementById('testFocusableTree1.node1').focus(); document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unfocusable element focus()ed after registered node focused returns original tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + assert.equal( this.focusManager.getFocusedTree(), this.testFocusableTree1, @@ -1149,7 +1376,7 @@ suite('FocusManager', function () { ); }); - test('unregistered tree focus()ed with prev node after unregistering still returns old tree', function () { + test('unregistered tree focus()ed with prev node after unregistering returns null', function () { this.focusManager.registerTree(this.testFocusableTree1); this.focusManager.registerTree(this.testFocusableTree2); document.getElementById('testFocusableTree1.node1').focus(); @@ -1158,10 +1385,46 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node1').focus(); - // Attempting to focus a now removed tree should have no effect. + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('nested tree focusTree()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + document.getElementById('testFocusableNestedTree4').focus(); + assert.equal( this.focusManager.getFocusedTree(), - this.testFocusableTree2, + this.testFocusableNestedTree4, + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, ); }); }); @@ -1263,12 +1526,21 @@ suite('FocusManager', function () { assert.isNull(this.focusManager.getFocusedNode()); }); - test('non-registered tree node focus()ed after registered node focused returns original node', function () { + test('non-registered tree node focus()ed after registered node focused returns null', function () { this.focusManager.registerTree(this.testFocusableTree1); document.getElementById('testFocusableTree1.node1').focus(); document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unfocuasble element focus()ed after registered node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + assert.equal( this.focusManager.getFocusedNode(), this.testFocusableTree1Node1, @@ -1320,7 +1592,7 @@ suite('FocusManager', function () { ); }); - test('unregistered tree focus()ed with prev node after unregistering still returns old node', function () { + test('unregistered tree focus()ed with prev node after unregistering returns null', function () { this.focusManager.registerTree(this.testFocusableTree1); this.focusManager.registerTree(this.testFocusableTree2); document.getElementById('testFocusableTree1.node1').focus(); @@ -1329,10 +1601,46 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node1').focus(); - // Attempting to focus a now removed tree should have no effect. + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('nested tree focus()ed with no prev focus returns nested root', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + document.getElementById('testFocusableNestedTree4').focus(); + assert.equal( this.focusManager.getFocusedNode(), - this.testFocusableTree2Node1, + this.testFocusableNestedTree4.getRootFocusableNode(), + ); + }); + + test('nested tree node focus()ed with no prev focus returns focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + }); + + test('nested tree node focus()ed after parent focused returns focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, ); }); }); @@ -1492,17 +1800,17 @@ suite('FocusManager', function () { ); }); - test('non-registered tree node focus()ed after registered node focused original node has active focus', function () { + test('unfocsable element focus()ed after registered node focused original node has active focus', function () { this.focusManager.registerTree(this.testFocusableTree1); document.getElementById('testFocusableTree1.node1').focus(); - document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + document.getElementById('testUnfocusableElement').focus(); // The original node should be unchanged, and the unregistered node should not have any // focus indicators. const nodeElem = document.getElementById('testFocusableTree1.node1'); const attemptedNewNodeElem = document.getElementById( - 'testUnregisteredFocusableTree3.node1', + 'testUnfocusableElement', ); assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); assert.notInclude( @@ -1615,7 +1923,7 @@ suite('FocusManager', function () { ); }); - test('unregistered tree focus()ed with prev node after unregistering does not change indicators', function () { + test('unregistered tree focus()ed with prev node after unregistering removes active indicator', function () { this.focusManager.registerTree(this.testFocusableTree1); this.focusManager.registerTree(this.testFocusableTree2); document.getElementById('testFocusableTree1.node1').focus(); @@ -1624,16 +1932,16 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node1').focus(); - // Attempting to focus a now removed tree should have no effect. + // Attempting to focus a now removed tree should remove active. const otherNodeElem = this.testFocusableTree2Node1.getFocusableElement(); const removedNodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.include( + assert.notInclude( Array.from(otherNodeElem.classList), 'blocklyActiveFocus', ); - assert.notInclude( + assert.include( Array.from(otherNodeElem.classList), 'blocklyPassiveFocus', ); @@ -1712,6 +2020,65 @@ suite('FocusManager', function () { 'blocklyPassiveFocus', ); }); + + test('nested tree focus()ed with no prev root has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + document.getElementById('testFocusableNestedTree4').focus(); + + const rootElem = this.testFocusableNestedTree4 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('nested tree node focus()ed with no prev focus node has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + const nodeElem = + this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('nested tree node focus()ed after parent focused prev has passive node has active', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + const prevNodeElem = this.testFocusableTree2Node1.getFocusableElement(); + const currNodeElem = + this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.include( + Array.from(prevNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.include( + Array.from(currNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(currNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); }); }); @@ -1887,6 +2254,43 @@ suite('FocusManager', function () { this.testFocusableGroup2, ); }); + + test('nested tree focusTree()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusTree(this.testFocusableNestedGroup4); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); }); suite('getFocusedNode()', function () { test('registered tree focusTree()ed no prev focus returns root node', function () { @@ -2059,6 +2463,43 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1, ); }); + + test('nested tree focusTree()ed with no prev focus returns nested root', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusTree(this.testFocusableNestedGroup4); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4.getRootFocusableNode(), + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns focused node', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4Node1, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns focused node', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4Node1, + ); + }); }); suite('CSS classes', function () { test('registered tree focusTree()ed no prev focus root elem has active property', function () { @@ -2402,6 +2843,66 @@ suite('FocusManager', function () { 'blocklyPassiveFocus', ); }); + + test('nested tree focusTree()ed with no prev root has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusTree(this.testFocusableNestedGroup4); + + const rootElem = this.testFocusableNestedGroup4 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('nested tree node focusNode()ed with no prev focus node has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + const nodeElem = + this.testFocusableNestedGroup4Node1.getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('nested tree node focusNode()ed after parent focused prev has passive node has active', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + const prevNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const currNodeElem = + this.testFocusableNestedGroup4Node1.getFocusableElement(); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.include( + Array.from(prevNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.include( + Array.from(currNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(currNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); }); }); @@ -2506,7 +3007,7 @@ suite('FocusManager', function () { assert.isNull(this.focusManager.getFocusedTree()); }); - test('non-registered tree node focus()ed after registered node focused returns original tree', function () { + test('non-registered tree node focus()ed after registered node focused returns null', function () { this.focusManager.registerTree(this.testFocusableGroup1); document.getElementById('testFocusableGroup1.node1').focus(); @@ -2514,10 +3015,10 @@ suite('FocusManager', function () { .getElementById('testUnregisteredFocusableGroup3.node1') .focus(); - assert.equal( - this.focusManager.getFocusedTree(), - this.testFocusableGroup1, - ); + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedTree()); }); test('unregistered tree focus()ed with no prev focus returns null', function () { @@ -2565,7 +3066,7 @@ suite('FocusManager', function () { ); }); - test('unregistered tree focus()ed with prev node after unregistering still returns old tree', function () { + test('unregistered tree focus()ed with prev node after unregistering returns null', function () { this.focusManager.registerTree(this.testFocusableGroup1); this.focusManager.registerTree(this.testFocusableGroup2); document.getElementById('testFocusableGroup1.node1').focus(); @@ -2574,10 +3075,46 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node1').focus(); - // Attempting to focus a now removed tree should have no effect. + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('nested tree focusTree()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + document.getElementById('testFocusableNestedGroup4').focus(); + assert.equal( this.focusManager.getFocusedTree(), - this.testFocusableGroup2, + this.testFocusableNestedGroup4, + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, ); }); }); @@ -2681,7 +3218,7 @@ suite('FocusManager', function () { assert.isNull(this.focusManager.getFocusedNode()); }); - test('non-registered tree node focus()ed after registered node focused returns original node', function () { + test('non-registered tree node focus()ed after registered node focused returns null', function () { this.focusManager.registerTree(this.testFocusableGroup1); document.getElementById('testFocusableGroup1.node1').focus(); @@ -2689,6 +3226,15 @@ suite('FocusManager', function () { .getElementById('testUnregisteredFocusableGroup3.node1') .focus(); + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unfocusable element focus()ed after registered node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + assert.equal( this.focusManager.getFocusedNode(), this.testFocusableGroup1Node1, @@ -2740,7 +3286,7 @@ suite('FocusManager', function () { ); }); - test('unregistered tree focus()ed with prev node after unregistering still returns old node', function () { + test('unregistered tree focus()ed with prev node after unregistering returns null', function () { this.focusManager.registerTree(this.testFocusableGroup1); this.focusManager.registerTree(this.testFocusableGroup2); document.getElementById('testFocusableGroup1.node1').focus(); @@ -2749,10 +3295,46 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node1').focus(); - // Attempting to focus a now removed tree should have no effect. + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('nested tree focus()ed with no prev focus returns nested root', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + document.getElementById('testFocusableNestedGroup4').focus(); + assert.equal( this.focusManager.getFocusedNode(), - this.testFocusableGroup2Node1, + this.testFocusableNestedGroup4.getRootFocusableNode(), + ); + }); + + test('nested tree node focus()ed with no prev focus returns focused node', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4Node1, + ); + }); + + test('nested tree node focus()ed after parent focused returns focused node', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4Node1, ); }); }); @@ -2916,19 +3498,17 @@ suite('FocusManager', function () { ); }); - test('non-registered tree node focus()ed after registered node focused original node has active focus', function () { + test('unfocusable element focus()ed after registered node focused original node has active focus', function () { this.focusManager.registerTree(this.testFocusableGroup1); document.getElementById('testFocusableGroup1.node1').focus(); - document - .getElementById('testUnregisteredFocusableGroup3.node1') - .focus(); + document.getElementById('testUnfocusableElement').focus(); // The original node should be unchanged, and the unregistered node should not have any // focus indicators. const nodeElem = document.getElementById('testFocusableGroup1.node1'); const attemptedNewNodeElem = document.getElementById( - 'testUnregisteredFocusableGroup3.node1', + 'testUnfocusableElement', ); assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); assert.notInclude( @@ -3041,7 +3621,7 @@ suite('FocusManager', function () { ); }); - test('unregistered tree focus()ed with prev node after unregistering does not change indicators', function () { + test('unregistered tree focus()ed with prev node after unregistering removes active indicator', function () { this.focusManager.registerTree(this.testFocusableGroup1); this.focusManager.registerTree(this.testFocusableGroup2); document.getElementById('testFocusableGroup1.node1').focus(); @@ -3050,16 +3630,16 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node1').focus(); - // Attempting to focus a now removed tree should have no effect. + // Attempting to focus a now removed tree should remove active. const otherNodeElem = this.testFocusableGroup2Node1.getFocusableElement(); const removedNodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.include( + assert.notInclude( Array.from(otherNodeElem.classList), 'blocklyActiveFocus', ); - assert.notInclude( + assert.include( Array.from(otherNodeElem.classList), 'blocklyPassiveFocus', ); @@ -3138,6 +3718,163 @@ suite('FocusManager', function () { 'blocklyPassiveFocus', ); }); + + test('nested tree focus()ed with no prev root has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + document.getElementById('testFocusableNestedGroup4').focus(); + + const rootElem = this.testFocusableNestedGroup4 + .getRootFocusableNode() + .getFocusableElement(); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(rootElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('nested tree node focus()ed with no prev focus node has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + const nodeElem = + this.testFocusableNestedGroup4Node1.getFocusableElement(); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notInclude( + Array.from(nodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + + test('nested tree node focus()ed after parent focused prev has passive node has active', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + const prevNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const currNodeElem = + this.testFocusableNestedGroup4Node1.getFocusableElement(); + assert.notInclude( + Array.from(prevNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.include( + Array.from(prevNodeElem.classList), + 'blocklyPassiveFocus', + ); + assert.include( + Array.from(currNodeElem.classList), + 'blocklyActiveFocus', + ); + assert.notInclude( + Array.from(currNodeElem.classList), + 'blocklyPassiveFocus', + ); + }); + }); + }); + + /* High-level focus/defocusing tests. */ + suite('Defocusing and refocusing', function () { + test('Defocusing actively focused root HTML tree switches to passive highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree2); + + document.getElementById('testUnregisteredFocusableTree3').focus(); + + const rootNode = this.testFocusableTree2.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + assert.isNull(this.focusManager.getFocusedTree()); + assert.isNull(this.focusManager.getFocusedNode()); + assert.include(Array.from(rootElem.classList), 'blocklyPassiveFocus'); + assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + }); + + test('Defocusing actively focused HTML tree node switches to passive highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + document.getElementById('testUnregisteredFocusableTree3').focus(); + + const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.isNull(this.focusManager.getFocusedTree()); + assert.isNull(this.focusManager.getFocusedNode()); + assert.include(Array.from(nodeElem.classList), 'blocklyPassiveFocus'); + assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + }); + + test('Defocusing actively focused HTML subtree node switches to passive highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + document.getElementById('testUnregisteredFocusableTree3').focus(); + + const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.isNull(this.focusManager.getFocusedTree()); + assert.isNull(this.focusManager.getFocusedNode()); + assert.include(Array.from(nodeElem.classList), 'blocklyPassiveFocus'); + assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + }); + + test('Refocusing actively focused root HTML tree restores to active highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree2); + document.getElementById('testUnregisteredFocusableTree3').focus(); + + document.getElementById('testFocusableTree2').focus(); + + const rootNode = this.testFocusableTree2.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + assert.equal(this.focusManager.getFocusedTree(), this.testFocusableTree2); + assert.equal(this.focusManager.getFocusedNode(), rootNode); + assert.notInclude(Array.from(rootElem.classList), 'blocklyPassiveFocus'); + assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + }); + + test('Refocusing actively focused HTML tree node restores to active highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + document.getElementById('testUnregisteredFocusableTree3').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.equal(this.focusManager.getFocusedTree(), this.testFocusableTree2); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.notInclude(Array.from(nodeElem.classList), 'blocklyPassiveFocus'); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + }); + + test('Refocusing actively focused HTML subtree node restores to active highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + document.getElementById('testUnregisteredFocusableTree3').focus(); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.equal( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + assert.equal( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + assert.notInclude(Array.from(nodeElem.classList), 'blocklyPassiveFocus'); + assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); }); }); diff --git a/tests/mocha/focusable_tree_traverser_test.js b/tests/mocha/focusable_tree_traverser_test.js new file mode 100644 index 00000000000..2069132fee7 --- /dev/null +++ b/tests/mocha/focusable_tree_traverser_test.js @@ -0,0 +1,480 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {FocusableTreeTraverser} from '../../build/src/core/utils/focusable_tree_traverser.js'; +import {assert} from '../../node_modules/chai/chai.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('FocusableTreeTraverser', function () { + setup(function () { + sharedTestSetup.call(this); + + const FocusableNodeImpl = function (element, tree) { + this.getFocusableElement = function () { + return element; + }; + + this.getFocusableTree = function () { + return tree; + }; + }; + const FocusableTreeImpl = function (rootElement, nestedTrees) { + this.idToNodeMap = {}; + + this.addNode = function (element) { + const node = new FocusableNodeImpl(element, this); + this.idToNodeMap[element.id] = node; + return node; + }; + + this.getFocusedNode = function () { + throw Error('Unused in test suite.'); + }; + + this.getRootFocusableNode = function () { + return this.rootNode; + }; + + this.getNestedTrees = function () { + return nestedTrees; + }; + + this.lookUpFocusableNode = function (id) { + return this.idToNodeMap[id]; + }; + + this.findFocusableNodeFor = function (element) { + return FocusableTreeTraverser.findFocusableNodeFor(element, this); + }; + + this.rootNode = this.addNode(rootElement); + }; + + const createFocusableTree = function (rootElementId, nestedTrees) { + return new FocusableTreeImpl( + document.getElementById(rootElementId), + nestedTrees || [], + ); + }; + const createFocusableNode = function (tree, elementId) { + return tree.addNode(document.getElementById(elementId)); + }; + + this.testFocusableTree1 = createFocusableTree('testFocusableTree1'); + this.testFocusableTree1Node1 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node1', + ); + this.testFocusableTree1Node1Child1 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node1.child1', + ); + this.testFocusableTree1Node2 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node2', + ); + this.testFocusableNestedTree4 = createFocusableTree( + 'testFocusableNestedTree4', + ); + this.testFocusableNestedTree4Node1 = createFocusableNode( + this.testFocusableNestedTree4, + 'testFocusableNestedTree4.node1', + ); + this.testFocusableNestedTree5 = createFocusableTree( + 'testFocusableNestedTree5', + ); + this.testFocusableNestedTree5Node1 = createFocusableNode( + this.testFocusableNestedTree5, + 'testFocusableNestedTree5.node1', + ); + this.testFocusableTree2 = createFocusableTree('testFocusableTree2', [ + this.testFocusableNestedTree4, + this.testFocusableNestedTree5, + ]); + this.testFocusableTree2Node1 = createFocusableNode( + this.testFocusableTree2, + 'testFocusableTree2.node1', + ); + }); + + teardown(function () { + sharedTestTeardown.call(this); + + const removeFocusIndicators = function (element) { + element.classList.remove('blocklyActiveFocus', 'blocklyPassiveFocus'); + }; + + // Ensure all node CSS styles are reset so that state isn't leaked between tests. + removeFocusIndicators(document.getElementById('testFocusableTree1')); + removeFocusIndicators(document.getElementById('testFocusableTree1.node1')); + removeFocusIndicators( + document.getElementById('testFocusableTree1.node1.child1'), + ); + removeFocusIndicators(document.getElementById('testFocusableTree1.node2')); + removeFocusIndicators(document.getElementById('testFocusableTree2')); + removeFocusIndicators(document.getElementById('testFocusableTree2.node1')); + removeFocusIndicators(document.getElementById('testFocusableNestedTree4')); + removeFocusIndicators( + document.getElementById('testFocusableNestedTree4.node1'), + ); + removeFocusIndicators(document.getElementById('testFocusableNestedTree5')); + removeFocusIndicators( + document.getElementById('testFocusableNestedTree5.node1'), + ); + }); + + suite('findFocusedNode()', function () { + test('for tree with no highlights returns null', function () { + const tree = this.testFocusableTree1; + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.isNull(finding); + }); + + test('for tree with root active highlight returns root node', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + rootNode.getFocusableElement().classList.add('blocklyActiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.equal(finding, rootNode); + }); + + test('for tree with root passive highlight returns root node', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + rootNode.getFocusableElement().classList.add('blocklyPassiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.equal(finding, rootNode); + }); + + test('for tree with node active highlight returns node', function () { + const tree = this.testFocusableTree1; + const node = this.testFocusableTree1Node1; + node.getFocusableElement().classList.add('blocklyActiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.equal(finding, node); + }); + + test('for tree with node passive highlight returns node', function () { + const tree = this.testFocusableTree1; + const node = this.testFocusableTree1Node1; + node.getFocusableElement().classList.add('blocklyPassiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.equal(finding, node); + }); + + test('for tree with nested node active highlight returns node', function () { + const tree = this.testFocusableTree1; + const node = this.testFocusableTree1Node1Child1; + node.getFocusableElement().classList.add('blocklyActiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.equal(finding, node); + }); + + test('for tree with nested node passive highlight returns node', function () { + const tree = this.testFocusableTree1; + const node = this.testFocusableTree1Node1Child1; + node.getFocusableElement().classList.add('blocklyPassiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.equal(finding, node); + }); + + test('for tree with nested tree root active no parent highlights returns null', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); + rootNode.getFocusableElement().classList.add('blocklyActiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.equal(finding, rootNode); + }); + + test('for tree with nested tree root passive no parent highlights returns null', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); + rootNode.getFocusableElement().classList.add('blocklyPassiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.equal(finding, rootNode); + }); + + test('for tree with nested tree root active no parent highlights returns null', function () { + const tree = this.testFocusableNestedTree4; + const node = this.testFocusableNestedTree4Node1; + node.getFocusableElement().classList.add('blocklyActiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.equal(finding, node); + }); + + test('for tree with nested tree root passive no parent highlights returns null', function () { + const tree = this.testFocusableNestedTree4; + const node = this.testFocusableNestedTree4Node1; + node.getFocusableElement().classList.add('blocklyPassiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.equal(finding, node); + }); + + test('for tree with nested tree root active parent node passive returns parent node', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); + this.testFocusableTree2Node1 + .getFocusableElement() + .classList.add('blocklyPassiveFocus'); + rootNode.getFocusableElement().classList.add('blocklyActiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode( + this.testFocusableTree2, + ); + + assert.equal(finding, this.testFocusableTree2Node1); + }); + + test('for tree with nested tree root passive parent node passive returns parent node', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); + this.testFocusableTree2Node1 + .getFocusableElement() + .classList.add('blocklyPassiveFocus'); + rootNode.getFocusableElement().classList.add('blocklyPassiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode( + this.testFocusableTree2, + ); + + assert.equal(finding, this.testFocusableTree2Node1); + }); + + test('for tree with nested tree node active parent node passive returns parent node', function () { + const tree = this.testFocusableNestedTree4; + const node = this.testFocusableNestedTree4Node1; + this.testFocusableTree2Node1 + .getFocusableElement() + .classList.add('blocklyPassiveFocus'); + node.getFocusableElement().classList.add('blocklyActiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode( + this.testFocusableTree2, + ); + + assert.equal(finding, this.testFocusableTree2Node1); + }); + + test('for tree with nested tree node passive parent node passive returns parent node', function () { + const tree = this.testFocusableNestedTree4; + const node = this.testFocusableNestedTree4Node1; + this.testFocusableTree2Node1 + .getFocusableElement() + .classList.add('blocklyPassiveFocus'); + node.getFocusableElement().classList.add('blocklyPassiveFocus'); + + const finding = FocusableTreeTraverser.findFocusedNode( + this.testFocusableTree2, + ); + + assert.equal(finding, this.testFocusableTree2Node1); + }); + }); + + suite('findFocusableNodeFor()', function () { + test('for root element returns root', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + + assert.equal(finding, rootNode); + }); + + test('for element for different tree root returns null', function () { + const tree = this.testFocusableTree1; + const rootNode = this.testFocusableTree2.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + + assert.isNull(finding); + }); + + test('for element for different tree node returns null', function () { + const tree = this.testFocusableTree1; + const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + assert.isNull(finding); + }); + + test('for node element in tree returns node', function () { + const tree = this.testFocusableTree1; + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + assert.equal(finding, this.testFocusableTree1Node1); + }); + + test('for non-node element in tree returns root', function () { + const tree = this.testFocusableTree1; + const unregElem = document.getElementById( + 'testFocusableTree1.node2.unregisteredChild1', + ); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + unregElem, + tree, + ); + + // An unregistered element should map to the closest node. + assert.equal(finding, this.testFocusableTree1Node2); + }); + + test('for nested node element in tree returns node', function () { + const tree = this.testFocusableTree1; + const nodeElem = this.testFocusableTree1Node1Child1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + // The nested node should be returned. + assert.equal(finding, this.testFocusableTree1Node1Child1); + }); + + test('for nested node element in tree returns node', function () { + const tree = this.testFocusableTree1; + const unregElem = document.getElementById( + 'testFocusableTree1.node1.child1.unregisteredChild1', + ); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + unregElem, + tree, + ); + + // An unregistered element should map to the closest node. + assert.equal(finding, this.testFocusableTree1Node1Child1); + }); + + test('for nested node element in tree returns node', function () { + const tree = this.testFocusableTree1; + const unregElem = document.getElementById( + 'testFocusableTree1.unregisteredChild1', + ); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + unregElem, + tree, + ); + + // An unregistered element should map to the closest node (or root). + assert.equal(finding, tree.getRootFocusableNode()); + }); + + test('for nested tree root returns nested tree root', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + + assert.equal(finding, rootNode); + }); + + test('for nested tree node returns nested tree node', function () { + const tree = this.testFocusableNestedTree4; + const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + // The node of the nested tree should be returned. + assert.equal(finding, this.testFocusableNestedTree4Node1); + }); + + test('for nested element in nested tree node returns nearest nested node', function () { + const tree = this.testFocusableNestedTree4; + const unregElem = document.getElementById( + 'testFocusableNestedTree4.node1.unregisteredChild1', + ); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + unregElem, + tree, + ); + + // An unregistered element should map to the closest node. + assert.equal(finding, this.testFocusableNestedTree4Node1); + }); + + test('for nested tree node under root with different tree base returns null', function () { + const tree = this.testFocusableTree2; + const nodeElem = this.testFocusableNestedTree5Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + // The nested node hierarchically sits below the outer tree, but using + // that tree as the basis should yield null since it's not a direct child. + assert.isNull(finding); + }); + + test('for nested tree node under node with different tree base returns null', function () { + const tree = this.testFocusableTree2; + const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + // The nested node hierarchically sits below the outer tree, but using + // that tree as the basis should yield null since it's not a direct child. + assert.isNull(finding); + }); + }); +}); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 2eb42869aad..17e15d2c78d 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -35,26 +35,57 @@ fill: #00f; } - +
-
+ Focusable tree 1 +
Tree 1 node 1 -
Tree 1 node 1 child 1
+
+ Tree 1 node 1 child 1 +
+ Tree 1 node 1 child 1 child 1 (unregistered) +
+
-
+
Tree 1 node 2 -
Tree 1 node 2 child 2 (unregistered)
+
+ Tree 1 node 2 child 2 (unregistered) +
+
+
+ Tree 1 child 1 (unregistered)
-
Tree 2 node 1
+ Focusable tree 2 +
+ Tree 2 node 1 +
+ Nested tree 4 +
+ Tree 4 node 1 (nested) +
+ Tree 4 node 1 child 1 (unregistered) +
+
+
+
+
+ Nested tree 5 +
Tree 5 node 1 (nested)
+
-
Tree 3 node 1 (unregistered)
+ Unregistered tree 3 +
+ Tree 3 node 1 (unregistered) +
+
Unfocusable element
@@ -71,7 +102,9 @@ Group 1 node 2 - Tree 1 node 2 child 2 (unregistered) + + Tree 1 node 2 child 2 (unregistered) + @@ -80,11 +113,19 @@ Group 2 node 1 + + + + Group 4 node 1 (nested) + + - - Tree 3 node 1 (unregistered) + + + Tree 3 node 1 (unregistered) + @@ -162,6 +203,7 @@ import './field_variable_test.js'; import './flyout_test.js'; import './focus_manager_test.js'; + import './focusable_tree_traverser_test.js'; // import './test_event_reduction.js'; import './generator_test.js'; import './gesture_test.js'; From 9ab77cedff1dbca50a5fb6bdff43016f876317ad Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Mar 2025 22:04:51 +0000 Subject: [PATCH 111/222] chore: fix formatting issues --- tests/mocha/index.html | 55 ++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 17e15d2c78d..6231da3eacc 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -41,47 +41,76 @@
Focusable tree 1 -
+
Tree 1 node 1 -
+
Tree 1 node 1 child 1 -
+
Tree 1 node 1 child 1 child 1 (unregistered)
-
+
Tree 1 node 2 -
+
Tree 1 node 2 child 2 (unregistered)
-
+
Tree 1 child 1 (unregistered)
Focusable tree 2 -
+
Tree 2 node 1 -
+
Nested tree 4 -
+
Tree 4 node 1 (nested) -
+
Tree 4 node 1 child 1 (unregistered)
-
+
Nested tree 5 -
Tree 5 node 1 (nested)
+
+ Tree 5 node 1 (nested) +
Unregistered tree 3 -
+
Tree 3 node 1 (unregistered)
From 3dc4d17b304b66cc2c22c2739fb8b2516afe72a2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Mar 2025 16:54:19 -0700 Subject: [PATCH 112/222] Update tests/mocha/index.html --- tests/mocha/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 6231da3eacc..690b75a7759 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -233,7 +233,6 @@ import './flyout_test.js'; import './focus_manager_test.js'; import './focusable_tree_traverser_test.js'; - // import './test_event_reduction.js'; import './generator_test.js'; import './gesture_test.js'; import './icon_test.js'; From 7a07b4b2ba60f653bff1c8f28c1b797271e3916b Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 28 Mar 2025 13:54:33 -0700 Subject: [PATCH 113/222] refactor!: Remove old cursor and tab support. (#8803) --- core/block_svg.ts | 32 ---- core/blockly.ts | 4 - core/field.ts | 9 - core/field_input.ts | 14 -- core/keyboard_nav/basic_cursor.ts | 222 ----------------------- core/keyboard_nav/tab_navigate_cursor.ts | 45 ----- 6 files changed, 326 deletions(-) delete mode 100644 core/keyboard_nav/basic_cursor.ts delete mode 100644 core/keyboard_nav/tab_navigate_cursor.ts diff --git a/core/block_svg.ts b/core/block_svg.ts index a33a21a8505..1b76ed3f1c4 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -34,7 +34,6 @@ import {BlockDragStrategy} from './dragging/block_drag_strategy.js'; import type {BlockMove} from './events/events_block_move.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; -import type {Field} from './field.js'; import {FieldLabel} from './field_label.js'; import {IconType} from './icons/icon_types.js'; import {MutatorIcon} from './icons/mutator_icon.js'; @@ -46,8 +45,6 @@ import type {ICopyable} from './interfaces/i_copyable.js'; import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js'; import {IIcon} from './interfaces/i_icon.js'; import * as internalConstants from './internal_constants.js'; -import {ASTNode} from './keyboard_nav/ast_node.js'; -import {TabNavigateCursor} from './keyboard_nav/tab_navigate_cursor.js'; import {MarkerManager} from './marker_manager.js'; import {Msg} from './msg.js'; import * as renderManagement from './render_management.js'; @@ -550,35 +547,6 @@ export class BlockSvg input.appendField(new FieldLabel(text), collapsedFieldName); } - /** - * Open the next (or previous) FieldTextInput. - * - * @param start Current field. - * @param forward If true go forward, otherwise backward. - */ - tab(start: Field, forward: boolean) { - const tabCursor = new TabNavigateCursor(); - tabCursor.setCurNode(ASTNode.createFieldNode(start)!); - const currentNode = tabCursor.getCurNode(); - - if (forward) { - tabCursor.next(); - } else { - tabCursor.prev(); - } - - const nextNode = tabCursor.getCurNode(); - if (nextNode && nextNode !== currentNode) { - const nextField = nextNode.getLocation() as Field; - nextField.showEditor(); - - // Also move the cursor if we're in keyboard nav mode. - if (this.workspace.keyboardAccessibilityMode) { - this.workspace.getCursor()!.setCurNode(nextNode); - } - } - } - /** * Handle a pointerdown on an SVG block. * diff --git a/core/blockly.ts b/core/blockly.ts index cf77bca3fb8..4c50c5ed5dd 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -169,10 +169,8 @@ import {IVariableMap} from './interfaces/i_variable_map.js'; import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import * as internalConstants from './internal_constants.js'; import {ASTNode} from './keyboard_nav/ast_node.js'; -import {BasicCursor} from './keyboard_nav/basic_cursor.js'; import {Cursor} from './keyboard_nav/cursor.js'; import {Marker} from './keyboard_nav/marker.js'; -import {TabNavigateCursor} from './keyboard_nav/tab_navigate_cursor.js'; import type {LayerManager} from './layer_manager.js'; import * as layers from './layers.js'; import {MarkerManager} from './marker_manager.js'; @@ -432,7 +430,6 @@ Names.prototype.populateProcedures = function ( // Re-export submodules that no longer declareLegacyNamespace. export { ASTNode, - BasicCursor, Block, BlockSvg, BlocklyOptions, @@ -589,7 +586,6 @@ export { ScrollbarPair, SeparatorFlyoutInflater, ShortcutRegistry, - TabNavigateCursor, Theme, ThemeManager, Toolbox, diff --git a/core/field.ts b/core/field.ts index deae29af90b..9dbd5a5cf91 100644 --- a/core/field.ts +++ b/core/field.ts @@ -1331,15 +1331,6 @@ export abstract class Field return false; } - /** - * Returns whether or not the field is tab navigable. - * - * @returns True if the field is tab navigable. - */ - isTabNavigable(): boolean { - return false; - } - /** * Handles the given keyboard shortcut. * diff --git a/core/field_input.ts b/core/field_input.ts index 98de56f7db3..ed97544dcc1 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -562,11 +562,6 @@ export abstract class FieldInput extends Field< ); WidgetDiv.hideIfOwner(this); dropDownDiv.hideWithoutAnimation(); - } else if (e.key === 'Tab') { - WidgetDiv.hideIfOwner(this); - dropDownDiv.hideWithoutAnimation(); - (this.sourceBlock_ as BlockSvg).tab(this, !e.shiftKey); - e.preventDefault(); } } @@ -674,15 +669,6 @@ export abstract class FieldInput extends Field< return true; } - /** - * Returns whether or not the field is tab navigable. - * - * @returns True if the field is tab navigable. - */ - override isTabNavigable(): boolean { - return true; - } - /** * Use the `getText_` developer hook to override the field's text * representation. When we're currently editing, return the current HTML value diff --git a/core/keyboard_nav/basic_cursor.ts b/core/keyboard_nav/basic_cursor.ts deleted file mode 100644 index 7526141529e..00000000000 --- a/core/keyboard_nav/basic_cursor.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * The class representing a basic cursor. - * Used to demo switching between different cursors. - * - * @class - */ -// Former goog.module ID: Blockly.BasicCursor - -import * as registry from '../registry.js'; -import {ASTNode} from './ast_node.js'; -import {Cursor} from './cursor.js'; - -/** - * Class for a basic cursor. - * This will allow the user to get to all nodes in the AST by hitting next or - * previous. - */ -export class BasicCursor extends Cursor { - /** Name used for registering a basic cursor. */ - static readonly registrationName = 'basicCursor'; - - constructor() { - super(); - } - - /** - * Find the next node in the pre order traversal. - * - * @returns The next node, or null if the current node is not set or there is - * no next value. - */ - override next(): ASTNode | null { - const curNode = this.getCurNode(); - if (!curNode) { - return null; - } - const newNode = this.getNextNode_(curNode, this.validNode_); - - if (newNode) { - this.setCurNode(newNode); - } - return newNode; - } - - /** - * For a basic cursor we only have the ability to go next and previous, so - * in will also allow the user to get to the next node in the pre order - * traversal. - * - * @returns The next node, or null if the current node is not set or there is - * no next value. - */ - override in(): ASTNode | null { - return this.next(); - } - - /** - * Find the previous node in the pre order traversal. - * - * @returns The previous node, or null if the current node is not set or there - * is no previous value. - */ - override prev(): ASTNode | null { - const curNode = this.getCurNode(); - if (!curNode) { - return null; - } - const newNode = this.getPreviousNode_(curNode, this.validNode_); - - if (newNode) { - this.setCurNode(newNode); - } - return newNode; - } - - /** - * For a basic cursor we only have the ability to go next and previous, so - * out will allow the user to get to the previous node in the pre order - * traversal. - * - * @returns The previous node, or null if the current node is not set or there - * is no previous value. - */ - override out(): ASTNode | null { - return this.prev(); - } - - /** - * Uses pre order traversal to navigate the Blockly AST. This will allow - * a user to easily navigate the entire Blockly AST without having to go in - * and out levels on the tree. - * - * @param node The current position in the AST. - * @param isValid A function true/false depending on whether the given node - * should be traversed. - * @returns The next node in the traversal. - */ - protected getNextNode_( - node: ASTNode | null, - isValid: (p1: ASTNode | null) => boolean, - ): ASTNode | null { - if (!node) { - return null; - } - const newNode = node.in() || node.next(); - if (isValid(newNode)) { - return newNode; - } else if (newNode) { - return this.getNextNode_(newNode, isValid); - } - const siblingOrParent = this.findSiblingOrParent(node.out()); - if (isValid(siblingOrParent)) { - return siblingOrParent; - } else if (siblingOrParent) { - return this.getNextNode_(siblingOrParent, isValid); - } - return null; - } - - /** - * Reverses the pre order traversal in order to find the previous node. This - * will allow a user to easily navigate the entire Blockly AST without having - * to go in and out levels on the tree. - * - * @param node The current position in the AST. - * @param isValid A function true/false depending on whether the given node - * should be traversed. - * @returns The previous node in the traversal or null if no previous node - * exists. - */ - protected getPreviousNode_( - node: ASTNode | null, - isValid: (p1: ASTNode | null) => boolean, - ): ASTNode | null { - if (!node) { - return null; - } - let newNode: ASTNode | null = node.prev(); - - if (newNode) { - newNode = this.getRightMostChild(newNode); - } else { - newNode = node.out(); - } - if (isValid(newNode)) { - return newNode; - } else if (newNode) { - return this.getPreviousNode_(newNode, isValid); - } - return null; - } - - /** - * Decides what nodes to traverse and which ones to skip. Currently, it - * skips output, stack and workspace nodes. - * - * @param node The AST node to check whether it is valid. - * @returns True if the node should be visited, false otherwise. - */ - protected validNode_(node: ASTNode | null): boolean { - let isValid = false; - const type = node && node.getType(); - if ( - type === ASTNode.types.OUTPUT || - type === ASTNode.types.INPUT || - type === ASTNode.types.FIELD || - type === ASTNode.types.NEXT || - type === ASTNode.types.PREVIOUS || - type === ASTNode.types.WORKSPACE - ) { - isValid = true; - } - return isValid; - } - - /** - * From the given node find either the next valid sibling or parent. - * - * @param node The current position in the AST. - * @returns The parent AST node or null if there are no valid parents. - */ - private findSiblingOrParent(node: ASTNode | null): ASTNode | null { - if (!node) { - return null; - } - const nextNode = node.next(); - if (nextNode) { - return nextNode; - } - return this.findSiblingOrParent(node.out()); - } - - /** - * Get the right most child of a node. - * - * @param node The node to find the right most child of. - * @returns The right most child of the given node, or the node if no child - * exists. - */ - private getRightMostChild(node: ASTNode | null): ASTNode | null { - if (!node!.in()) { - return node; - } - let newNode = node!.in(); - while (newNode && newNode.next()) { - newNode = newNode.next(); - } - return this.getRightMostChild(newNode); - } -} - -registry.register( - registry.Type.CURSOR, - BasicCursor.registrationName, - BasicCursor, -); diff --git a/core/keyboard_nav/tab_navigate_cursor.ts b/core/keyboard_nav/tab_navigate_cursor.ts deleted file mode 100644 index 0392887a1fd..00000000000 --- a/core/keyboard_nav/tab_navigate_cursor.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * The class representing a cursor that is used to navigate - * between tab navigable fields. - * - * @class - */ -// Former goog.module ID: Blockly.TabNavigateCursor - -import type {Field} from '../field.js'; -import {ASTNode} from './ast_node.js'; -import {BasicCursor} from './basic_cursor.js'; - -/** - * A cursor for navigating between tab navigable fields. - */ -export class TabNavigateCursor extends BasicCursor { - /** - * Skip all nodes except for tab navigable fields. - * - * @param node The AST node to check whether it is valid. - * @returns True if the node should be visited, false otherwise. - */ - override validNode_(node: ASTNode | null): boolean { - let isValid = false; - const type = node && node.getType(); - if (node) { - const location = node.getLocation() as Field; - if ( - type === ASTNode.types.FIELD && - location && - location.isTabNavigable() && - location.isClickable() - ) { - isValid = true; - } - } - return isValid; - } -} From 212f13af083004368abb5c5f4e46da6a41995b70 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Fri, 28 Mar 2025 14:02:28 -0700 Subject: [PATCH 114/222] feat: Add a method for creating flyout options (#8829) --- core/toolbox/toolbox.ts | 16 +--------------- core/trashcan.ts | 15 +-------------- core/workspace_svg.ts | 23 +++++++++++++++++------ 3 files changed, 19 insertions(+), 35 deletions(-) diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 556bc89e685..8d4f4c9c8c8 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -13,7 +13,6 @@ // Unused import preserved for side-effects. Remove if unneeded. import {BlockSvg} from '../block_svg.js'; -import type {BlocklyOptions} from '../blockly_options.js'; import * as browserEvents from '../browser_events.js'; import * as common from '../common.js'; import {ComponentManager} from '../component_manager.js'; @@ -33,7 +32,6 @@ import {isSelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.j import type {IStyleable} from '../interfaces/i_styleable.js'; import type {IToolbox} from '../interfaces/i_toolbox.js'; import type {IToolboxItem} from '../interfaces/i_toolbox_item.js'; -import {Options} from '../options.js'; import * as registry from '../registry.js'; import type {KeyboardShortcut} from '../shortcut_registry.js'; import * as Touch from '../touch.js'; @@ -325,19 +323,7 @@ export class Toolbox */ protected createFlyout_(): IFlyout { const workspace = this.workspace_; - // TODO (#4247): Look into adding a makeFlyout method to Blockly Options. - const workspaceOptions = new Options({ - 'parentWorkspace': workspace, - 'rtl': workspace.RTL, - 'oneBasedIndex': workspace.options.oneBasedIndex, - 'horizontalLayout': workspace.horizontalLayout, - 'renderer': workspace.options.renderer, - 'rendererOverrides': workspace.options.rendererOverrides, - 'modalInputs': workspace.options.modalInputs, - 'move': { - 'scrollbars': true, - }, - } as BlocklyOptions); + const workspaceOptions = workspace.copyOptionsForFlyout(); // Options takes in either 'end' or 'start'. This has already been parsed to // be either 0 or 1, so set it after. workspaceOptions.toolboxPosition = workspace.options.toolboxPosition; diff --git a/core/trashcan.ts b/core/trashcan.ts index fdff7c50b48..0e80913f3ef 100644 --- a/core/trashcan.ts +++ b/core/trashcan.ts @@ -12,7 +12,6 @@ // Former goog.module ID: Blockly.Trashcan // Unused import preserved for side-effects. Remove if unneeded. -import type {BlocklyOptions} from './blockly_options.js'; import * as browserEvents from './browser_events.js'; import {ComponentManager} from './component_manager.js'; import {DeleteArea} from './delete_area.js'; @@ -26,7 +25,6 @@ import type {IDraggable} from './interfaces/i_draggable.js'; import type {IFlyout} from './interfaces/i_flyout.js'; import type {IPositionable} from './interfaces/i_positionable.js'; import type {UiMetrics} from './metrics_manager.js'; -import {Options} from './options.js'; import * as uiPosition from './positionable_helpers.js'; import * as registry from './registry.js'; import type * as blocks from './serialization/blocks.js'; @@ -103,18 +101,7 @@ export class Trashcan } // Create flyout options. - const flyoutWorkspaceOptions = new Options({ - 'scrollbars': true, - 'parentWorkspace': this.workspace, - 'rtl': this.workspace.RTL, - 'oneBasedIndex': this.workspace.options.oneBasedIndex, - 'renderer': this.workspace.options.renderer, - 'rendererOverrides': this.workspace.options.rendererOverrides, - 'modalInputs': this.workspace.options.modalInputs, - 'move': { - 'scrollbars': true, - }, - } as BlocklyOptions); + const flyoutWorkspaceOptions = this.workspace.copyOptionsForFlyout(); // Create vertical or horizontal flyout. if (this.workspace.horizontalLayout) { flyoutWorkspaceOptions.toolboxPosition = diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 94254a1ba20..58ef928064c 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -939,25 +939,36 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { } /** - * Add a flyout element in an element with the given tag name. + * Creates a new set of options from this workspace's options with just the + * values that are relevant to a flyout. * - * @param tagName What type of tag the flyout belongs in. - * @returns The element containing the flyout DOM. - * @internal + * @returns A subset of this workspace's options. */ - addFlyout(tagName: string | Svg | Svg): Element { - const workspaceOptions = new Options({ + copyOptionsForFlyout(): Options { + return new Options({ 'parentWorkspace': this, 'rtl': this.RTL, 'oneBasedIndex': this.options.oneBasedIndex, 'horizontalLayout': this.horizontalLayout, 'renderer': this.options.renderer, 'rendererOverrides': this.options.rendererOverrides, + 'plugins': this.options.plugins, 'modalInputs': this.options.modalInputs, 'move': { 'scrollbars': true, }, } as BlocklyOptions); + } + + /** + * Add a flyout element in an element with the given tag name. + * + * @param tagName What type of tag the flyout belongs in. + * @returns The element containing the flyout DOM. + * @internal + */ + addFlyout(tagName: string | Svg | Svg): Element { + const workspaceOptions = this.copyOptionsForFlyout(); workspaceOptions.toolboxPosition = this.options.toolboxPosition; if (this.horizontalLayout) { const HorizontalFlyout = registry.getClassFromOptions( From d85fcef1d849d7c2254b0583e07ded6b390e87a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:53:41 +0000 Subject: [PATCH 115/222] chore(deps): bump eslint-plugin-jsdoc from 50.6.0 to 50.6.9 Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 50.6.0 to 50.6.9. - [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases) - [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc) - [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v50.6.0...v50.6.9) --- updated-dependencies: - dependency-name: eslint-plugin-jsdoc dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59c06f90b4e..b3123e79c71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3891,10 +3891,11 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "50.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.0.tgz", - "integrity": "sha512-tCNp4fR79Le3dYTPB0dKEv7yFyvGkUCa+Z3yuTrrNGGOxBlXo9Pn0PEgroOZikUQOGjxoGMVKNjrOHcYEdfszg==", + "version": "50.6.9", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.9.tgz", + "integrity": "sha512-7/nHu3FWD4QRG8tCVqcv+BfFtctUtEDWc29oeDXB4bwmDM2/r1ndl14AG/2DUntdqH7qmpvdemJKwb3R97/QEw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@es-joy/jsdoccomment": "~0.49.0", "are-docs-informative": "^0.0.2", From 717135099205a482a85eaafceaa1e4edf67ab125 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 1 Apr 2025 14:59:40 -0700 Subject: [PATCH 116/222] fix!: Tighten and correct typings on ASTNode (#8835) * fix!: Tighten typings on ASTNode.create*Node() methods. * fix: Restore missing condition. * fix: Fix unsafe casts, non-null assertions and incorrect types. * refactor: Simplify parent input checks. --- core/keyboard_nav/ast_node.ts | 103 ++++++++++------------------ core/renderers/common/marker_svg.ts | 4 +- 2 files changed, 38 insertions(+), 69 deletions(-) diff --git a/core/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts index ec46a4d3fd4..0de0ffb57f0 100644 --- a/core/keyboard_nav/ast_node.ts +++ b/core/keyboard_nav/ast_node.ts @@ -47,9 +47,7 @@ export class ASTNode { private readonly location: IASTNodeLocation; /** The coordinate on the workspace. */ - // AnyDuringMigration because: Type 'null' is not assignable to type - // 'Coordinate'. - private wsCoordinate: Coordinate = null as AnyDuringMigration; + private wsCoordinate: Coordinate | null = null; /** * @param type The type of the location. @@ -119,7 +117,7 @@ export class ASTNode { * @returns The workspace coordinate or null if the location is not a * workspace. */ - getWsCoordinate(): Coordinate { + getWsCoordinate(): Coordinate | null { return this.wsCoordinate; } @@ -144,12 +142,12 @@ export class ASTNode { private findNextForInput(): ASTNode | null { const location = this.location as Connection; const parentInput = location.getParentInput(); - const block = parentInput!.getSourceBlock(); - // AnyDuringMigration because: Argument of type 'Input | null' is not - // assignable to parameter of type 'Input'. - const curIdx = block!.inputList.indexOf(parentInput as AnyDuringMigration); - for (let i = curIdx + 1; i < block!.inputList.length; i++) { - const input = block!.inputList[i]; + const block = parentInput?.getSourceBlock(); + if (!block || !parentInput) return null; + + const curIdx = block.inputList.indexOf(parentInput); + for (let i = curIdx + 1; i < block.inputList.length; i++) { + const input = block.inputList[i]; const fieldRow = input.fieldRow; for (let j = 0; j < fieldRow.length; j++) { const field = fieldRow[j]; @@ -209,12 +207,12 @@ export class ASTNode { private findPrevForInput(): ASTNode | null { const location = this.location as Connection; const parentInput = location.getParentInput(); - const block = parentInput!.getSourceBlock(); - // AnyDuringMigration because: Argument of type 'Input | null' is not - // assignable to parameter of type 'Input'. - const curIdx = block!.inputList.indexOf(parentInput as AnyDuringMigration); + const block = parentInput?.getSourceBlock(); + if (!block || !parentInput) return null; + + const curIdx = block.inputList.indexOf(parentInput); for (let i = curIdx; i >= 0; i--) { - const input = block!.inputList[i]; + const input = block.inputList[i]; if (input.connection && input !== parentInput) { return ASTNode.createInputNode(input); } @@ -440,18 +438,11 @@ export class ASTNode { // substack. const topBlock = block.getTopStackBlock(); const topConnection = getParentConnection(topBlock); + const parentInput = topConnection?.targetConnection?.getParentInput(); // If the top connection has a parentInput, create an AST node pointing to // that input. - if ( - topConnection && - topConnection.targetConnection && - topConnection.targetConnection.getParentInput() - ) { - // AnyDuringMigration because: Argument of type 'Input | null' is not - // assignable to parameter of type 'Input'. - return ASTNode.createInputNode( - topConnection.targetConnection.getParentInput() as AnyDuringMigration, - ); + if (parentInput) { + return ASTNode.createInputNode(parentInput); } else { // Go to stack level if you are not underneath an input. return ASTNode.createStackNode(topBlock); @@ -538,7 +529,9 @@ export class ASTNode { case ASTNode.types.NEXT: { const connection = this.location as Connection; const targetConnection = connection.targetConnection; - return ASTNode.createConnectionNode(targetConnection!); + return targetConnection + ? ASTNode.createConnectionNode(targetConnection) + : null; } case ASTNode.types.BUTTON: return this.navigateFlyoutContents(true); @@ -575,7 +568,9 @@ export class ASTNode { case ASTNode.types.INPUT: { const connection = this.location as Connection; const targetConnection = connection.targetConnection; - return ASTNode.createConnectionNode(targetConnection!); + return targetConnection + ? ASTNode.createConnectionNode(targetConnection) + : null; } } @@ -708,10 +703,7 @@ export class ASTNode { * @param field The location of the AST node. * @returns An AST node pointing to a field. */ - static createFieldNode(field: Field): ASTNode | null { - if (!field) { - return null; - } + static createFieldNode(field: Field): ASTNode { return new ASTNode(ASTNode.types.FIELD, field); } @@ -724,25 +716,14 @@ export class ASTNode { * @returns An AST node pointing to a connection. */ static createConnectionNode(connection: Connection): ASTNode | null { - if (!connection) { - return null; - } const type = connection.type; - if (type === ConnectionType.INPUT_VALUE) { - // AnyDuringMigration because: Argument of type 'Input | null' is not - // assignable to parameter of type 'Input'. - return ASTNode.createInputNode( - connection.getParentInput() as AnyDuringMigration, - ); - } else if ( - type === ConnectionType.NEXT_STATEMENT && - connection.getParentInput() + const parentInput = connection.getParentInput(); + if ( + (type === ConnectionType.INPUT_VALUE || + type === ConnectionType.NEXT_STATEMENT) && + parentInput ) { - // AnyDuringMigration because: Argument of type 'Input | null' is not - // assignable to parameter of type 'Input'. - return ASTNode.createInputNode( - connection.getParentInput() as AnyDuringMigration, - ); + return ASTNode.createInputNode(parentInput); } else if (type === ConnectionType.NEXT_STATEMENT) { return new ASTNode(ASTNode.types.NEXT, connection); } else if (type === ConnectionType.OUTPUT_VALUE) { @@ -761,7 +742,7 @@ export class ASTNode { * @returns An AST node pointing to a input. */ static createInputNode(input: Input): ASTNode | null { - if (!input || !input.connection) { + if (!input.connection) { return null; } return new ASTNode(ASTNode.types.INPUT, input.connection); @@ -773,10 +754,7 @@ export class ASTNode { * @param block The block used to create an AST node. * @returns An AST node pointing to a block. */ - static createBlockNode(block: Block): ASTNode | null { - if (!block) { - return null; - } + static createBlockNode(block: Block): ASTNode { return new ASTNode(ASTNode.types.BLOCK, block); } @@ -790,10 +768,7 @@ export class ASTNode { * @returns An AST node of type stack that points to the top block on the * stack. */ - static createStackNode(topBlock: Block): ASTNode | null { - if (!topBlock) { - return null; - } + static createStackNode(topBlock: Block): ASTNode { return new ASTNode(ASTNode.types.STACK, topBlock); } @@ -806,10 +781,7 @@ export class ASTNode { * @returns An AST node of type stack that points to the top block on the * stack. */ - static createButtonNode(button: FlyoutButton): ASTNode | null { - if (!button) { - return null; - } + static createButtonNode(button: FlyoutButton): ASTNode { return new ASTNode(ASTNode.types.BUTTON, button); } @@ -822,12 +794,9 @@ export class ASTNode { * workspace. */ static createWorkspaceNode( - workspace: Workspace | null, - wsCoordinate: Coordinate | null, - ): ASTNode | null { - if (!wsCoordinate || !workspace) { - return null; - } + workspace: Workspace, + wsCoordinate: Coordinate, + ): ASTNode { const params = {wsCoordinate}; return new ASTNode(ASTNode.types.WORKSPACE, workspace, params); } diff --git a/core/renderers/common/marker_svg.ts b/core/renderers/common/marker_svg.ts index 77d35883c3e..4805e70400a 100644 --- a/core/renderers/common/marker_svg.ts +++ b/core/renderers/common/marker_svg.ts @@ -287,8 +287,8 @@ export class MarkerSvg { */ protected showWithCoordinates_(curNode: ASTNode) { const wsCoordinate = curNode.getWsCoordinate(); - let x = wsCoordinate.x; - const y = wsCoordinate.y; + let x = wsCoordinate?.x ?? 0; + const y = wsCoordinate?.y ?? 0; if (this.workspace.RTL) { x -= this.constants_.CURSOR_WS_WIDTH; From ca362725eef3d8f455ec9479425e6760a4de1198 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 3 Apr 2025 12:15:17 -0700 Subject: [PATCH 117/222] refactor!: Backport LineCursor to core. (#8834) * refactor: Backport LineCursor to core. * fix: Fix instantiation of LineCursor. * fix: Fix tests. * chore: Assauge the linter. * chore: Fix some typos. * feat: Make padding configurable for scrollBoundsIntoView. * chore: Merge in the latest changes from keyboard-experimentation. * refactor: Clarify name and docs for findSiblingOrParentSibling(). * fix: Improve scrollBoundsIntoView() behavior. * fix: Export CursorOptions. * refactor: Further clarify second parameter of setCurNode(). * fix: Revert change that could prevent scrolling bounds into view. --- core/blockly.ts | 5 +- core/field.ts | 4 +- core/field_input.ts | 2 +- core/keyboard_nav/cursor.ts | 137 ----- core/keyboard_nav/line_cursor.ts | 761 ++++++++++++++++++++++++++++ core/marker_manager.ts | 8 +- core/registry.ts | 4 +- core/renderers/zelos/path_object.ts | 3 +- core/workspace_svg.ts | 71 ++- tests/mocha/cursor_test.js | 33 +- 10 files changed, 861 insertions(+), 167 deletions(-) delete mode 100644 core/keyboard_nav/cursor.ts create mode 100644 core/keyboard_nav/line_cursor.ts diff --git a/core/blockly.ts b/core/blockly.ts index 4c50c5ed5dd..e14a89e74a7 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -169,7 +169,7 @@ import {IVariableMap} from './interfaces/i_variable_map.js'; import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import * as internalConstants from './internal_constants.js'; import {ASTNode} from './keyboard_nav/ast_node.js'; -import {Cursor} from './keyboard_nav/cursor.js'; +import {CursorOptions, LineCursor} from './keyboard_nav/line_cursor.js'; import {Marker} from './keyboard_nav/marker.js'; import type {LayerManager} from './layer_manager.js'; import * as layers from './layers.js'; @@ -444,11 +444,12 @@ export { ContextMenuItems, ContextMenuRegistry, Css, - Cursor, + CursorOptions, DeleteArea, DragTarget, Events, Extensions, + LineCursor, Procedures, ShortcutItems, Themes, diff --git a/core/field.ts b/core/field.ts index 9dbd5a5cf91..725a2867d9e 100644 --- a/core/field.ts +++ b/core/field.ts @@ -336,8 +336,10 @@ export abstract class Field * intend because the behavior was kind of hacked in. If you are thinking * about overriding this function, post on the forum with your intended * behavior to see if there's another approach. + * + * @internal */ - protected isFullBlockField(): boolean { + isFullBlockField(): boolean { return !this.borderRect_; } diff --git a/core/field_input.ts b/core/field_input.ts index ed97544dcc1..2cdd8056553 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -152,7 +152,7 @@ export abstract class FieldInput extends Field< } } - protected override isFullBlockField(): boolean { + override isFullBlockField(): boolean { const block = this.getSourceBlock(); if (!block) throw new UnattachedFieldError(); diff --git a/core/keyboard_nav/cursor.ts b/core/keyboard_nav/cursor.ts deleted file mode 100644 index 92279da562d..00000000000 --- a/core/keyboard_nav/cursor.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * The class representing a cursor. - * Used primarily for keyboard navigation. - * - * @class - */ -// Former goog.module ID: Blockly.Cursor - -import * as registry from '../registry.js'; -import {ASTNode} from './ast_node.js'; -import {Marker} from './marker.js'; - -/** - * Class for a cursor. - * A cursor controls how a user navigates the Blockly AST. - */ -export class Cursor extends Marker { - override type = 'cursor'; - - constructor() { - super(); - } - - /** - * Find the next connection, field, or block. - * - * @returns The next element, or null if the current node is not set or there - * is no next value. - */ - next(): ASTNode | null { - const curNode = this.getCurNode(); - if (!curNode) { - return null; - } - - let newNode = curNode.next(); - while ( - newNode && - newNode.next() && - (newNode.getType() === ASTNode.types.NEXT || - newNode.getType() === ASTNode.types.BLOCK) - ) { - newNode = newNode.next(); - } - - if (newNode) { - this.setCurNode(newNode); - } - return newNode; - } - - /** - * Find the in connection or field. - * - * @returns The in element, or null if the current node is not set or there is - * no in value. - */ - in(): ASTNode | null { - let curNode: ASTNode | null = this.getCurNode(); - if (!curNode) { - return null; - } - // If we are on a previous or output connection, go to the block level - // before performing next operation. - if ( - curNode.getType() === ASTNode.types.PREVIOUS || - curNode.getType() === ASTNode.types.OUTPUT - ) { - curNode = curNode.next(); - } - const newNode = curNode?.in() ?? null; - - if (newNode) { - this.setCurNode(newNode); - } - return newNode; - } - - /** - * Find the previous connection, field, or block. - * - * @returns The previous element, or null if the current node is not set or - * there is no previous value. - */ - prev(): ASTNode | null { - const curNode = this.getCurNode(); - if (!curNode) { - return null; - } - let newNode = curNode.prev(); - - while ( - newNode && - newNode.prev() && - (newNode.getType() === ASTNode.types.NEXT || - newNode.getType() === ASTNode.types.BLOCK) - ) { - newNode = newNode.prev(); - } - - if (newNode) { - this.setCurNode(newNode); - } - return newNode; - } - - /** - * Find the out connection, field, or block. - * - * @returns The out element, or null if the current node is not set or there - * is no out value. - */ - out(): ASTNode | null { - const curNode = this.getCurNode(); - if (!curNode) { - return null; - } - let newNode = curNode.out(); - - if (newNode && newNode.getType() === ASTNode.types.BLOCK) { - newNode = newNode.prev() || newNode; - } - - if (newNode) { - this.setCurNode(newNode); - } - return newNode; - } -} - -registry.register(registry.Type.CURSOR, registry.DEFAULT, Cursor); diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts new file mode 100644 index 00000000000..8f3ac19b423 --- /dev/null +++ b/core/keyboard_nav/line_cursor.ts @@ -0,0 +1,761 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The class representing a line cursor. + * A line cursor tries to traverse the blocks and connections on a block as if + * they were lines of code in a text editor. Previous and next traverse previous + * connections, next connections and blocks, while in and out traverse input + * connections and fields. + * @author aschmiedt@google.com (Abby Schmiedt) + */ + +import type {Block} from '../block.js'; +import {BlockSvg} from '../block_svg.js'; +import * as common from '../common.js'; +import type {Connection} from '../connection.js'; +import {ConnectionType} from '../connection_type.js'; +import type {Abstract} from '../events/events_abstract.js'; +import {Click, ClickTarget} from '../events/events_click.js'; +import {EventType} from '../events/type.js'; +import * as eventUtils from '../events/utils.js'; +import type {Field} from '../field.js'; +import * as registry from '../registry.js'; +import type {MarkerSvg} from '../renderers/common/marker_svg.js'; +import type {PathObject} from '../renderers/zelos/path_object.js'; +import {Renderer} from '../renderers/zelos/renderer.js'; +import * as dom from '../utils/dom.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import {ASTNode} from './ast_node.js'; +import {Marker} from './marker.js'; + +/** Options object for LineCursor instances. */ +export interface CursorOptions { + /** + * Can the cursor visit all stack connections (next/previous), or + * (if false) only unconnected next connections? + */ + stackConnections: boolean; +} + +/** Default options for LineCursor instances. */ +const defaultOptions: CursorOptions = { + stackConnections: true, +}; + +/** + * Class for a line cursor. + */ +export class LineCursor extends Marker { + override type = 'cursor'; + + /** Options for this line cursor. */ + private readonly options: CursorOptions; + + /** Locations to try moving the cursor to after a deletion. */ + private potentialNodes: ASTNode[] | null = null; + + /** Whether the renderer is zelos-style. */ + private isZelos = false; + + /** + * @param workspace The workspace this cursor belongs to. + * @param options Cursor options. + */ + constructor( + private readonly workspace: WorkspaceSvg, + options?: Partial, + ) { + super(); + // Bind changeListener to facilitate future disposal. + this.changeListener = this.changeListener.bind(this); + this.workspace.addChangeListener(this.changeListener); + // Regularise options and apply defaults. + this.options = {...defaultOptions, ...options}; + + this.isZelos = workspace.getRenderer() instanceof Renderer; + } + + /** + * Clean up this cursor. + */ + dispose() { + this.workspace.removeChangeListener(this.changeListener); + super.dispose(); + } + + /** + * Moves the cursor to the next previous connection, next connection or block + * in the pre order traversal. Finds the next node in the pre order traversal. + * + * @returns The next node, or null if the current node is + * not set or there is no next value. + */ + next(): ASTNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + const newNode = this.getNextNode(curNode, this.validLineNode.bind(this)); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + + /** + * Moves the cursor to the next input connection or field + * in the pre order traversal. + * + * @returns The next node, or null if the current node is + * not set or there is no next value. + */ + in(): ASTNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + const newNode = this.getNextNode(curNode, this.validInLineNode.bind(this)); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + /** + * Moves the cursor to the previous next connection or previous connection in + * the pre order traversal. + * + * @returns The previous node, or null if the current node + * is not set or there is no previous value. + */ + prev(): ASTNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + const newNode = this.getPreviousNode( + curNode, + this.validLineNode.bind(this), + ); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + + /** + * Moves the cursor to the previous input connection or field in the pre order + * traversal. + * + * @returns The previous node, or null if the current node + * is not set or there is no previous value. + */ + out(): ASTNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + const newNode = this.getPreviousNode( + curNode, + this.validInLineNode.bind(this), + ); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + + /** + * Returns true iff the node to which we would navigate if in() were + * called, which will be a validInLineNode, is also a validLineNode + * - in effect, if the LineCursor is at the end of the 'current + * line' of the program. + */ + atEndOfLine(): boolean { + const curNode = this.getCurNode(); + if (!curNode) return false; + const rightNode = this.getNextNode( + curNode, + this.validInLineNode.bind(this), + ); + return this.validLineNode(rightNode); + } + + /** + * Returns true iff the given node represents the "beginning of a + * new line of code" (and thus can be visited by pressing the + * up/down arrow keys). Specifically, if the node is for: + * + * - Any block that is not a value block. + * - A top-level value block (one that is unconnected). + * - An unconnected next statement input. + * - An unconnected 'next' connection - the "blank line at the end". + * This is to facilitate connecting additional blocks to a + * stack/substack. + * + * If options.stackConnections is true (the default) then allow the + * cursor to visit all (useful) stack connection by additionally + * returning true for: + * + * - Any next statement input + * - Any 'next' connection. + * - An unconnected previous statement input. + * + * @param node The AST node to check. + * @returns True if the node should be visited, false otherwise. + */ + protected validLineNode(node: ASTNode | null): boolean { + if (!node) return false; + const location = node.getLocation(); + const type = node && node.getType(); + switch (type) { + case ASTNode.types.BLOCK: + return !(location as Block).outputConnection?.isConnected(); + case ASTNode.types.INPUT: { + const connection = location as Connection; + return ( + connection.type === ConnectionType.NEXT_STATEMENT && + (this.options.stackConnections || !connection.isConnected()) + ); + } + case ASTNode.types.NEXT: + return ( + this.options.stackConnections || + !(location as Connection).isConnected() + ); + case ASTNode.types.PREVIOUS: + return ( + this.options.stackConnections && + !(location as Connection).isConnected() + ); + default: + return false; + } + } + + /** + * Returns true iff the given node can be visited by the cursor when + * using the left/right arrow keys. Specifically, if the node is + * any node for which valideLineNode would return true, plus: + * + * - Any block. + * - Any field that is not a full block field. + * - Any unconnected next or input connection. This is to + * facilitate connecting additional blocks. + * + * @param node The AST node to check whether it is valid. + * @returns True if the node should be visited, false otherwise. + */ + protected validInLineNode(node: ASTNode | null): boolean { + if (!node) return false; + if (this.validLineNode(node)) return true; + const location = node.getLocation(); + const type = node && node.getType(); + switch (type) { + case ASTNode.types.BLOCK: + return true; + case ASTNode.types.INPUT: + return !(location as Connection).isConnected(); + case ASTNode.types.FIELD: { + const field = node.getLocation() as Field; + return !( + field.getSourceBlock()?.isSimpleReporter() && field.isFullBlockField() + ); + } + default: + return false; + } + } + + /** + * Returns true iff the given node can be visited by the cursor. + * Specifically, if the node is any for which validInLineNode would + * return true, or if it is a workspace node. + * + * @param node The AST node to check whether it is valid. + * @returns True if the node should be visited, false otherwise. + */ + protected validNode(node: ASTNode | null): boolean { + return ( + !!node && + (this.validInLineNode(node) || node.getType() === ASTNode.types.WORKSPACE) + ); + } + + /** + * Uses pre order traversal to navigate the Blockly AST. This will allow + * a user to easily navigate the entire Blockly AST without having to go in + * and out levels on the tree. + * + * @param node The current position in the AST. + * @param isValid A function true/false depending on whether the given node + * should be traversed. + * @returns The next node in the traversal. + */ + private getNextNode( + node: ASTNode | null, + isValid: (p1: ASTNode | null) => boolean, + ): ASTNode | null { + if (!node) { + return null; + } + const newNode = node.in() || node.next(); + if (isValid(newNode)) { + return newNode; + } else if (newNode) { + return this.getNextNode(newNode, isValid); + } + const siblingOrParentSibling = this.findSiblingOrParentSibling(node.out()); + if (isValid(siblingOrParentSibling)) { + return siblingOrParentSibling; + } else if (siblingOrParentSibling) { + return this.getNextNode(siblingOrParentSibling, isValid); + } + return null; + } + + /** + * Reverses the pre order traversal in order to find the previous node. This + * will allow a user to easily navigate the entire Blockly AST without having + * to go in and out levels on the tree. + * + * @param node The current position in the AST. + * @param isValid A function true/false depending on whether the given node + * should be traversed. + * @returns The previous node in the traversal or null if no previous node + * exists. + */ + private getPreviousNode( + node: ASTNode | null, + isValid: (p1: ASTNode | null) => boolean, + ): ASTNode | null { + if (!node) { + return null; + } + let newNode: ASTNode | null = node.prev(); + + if (newNode) { + newNode = this.getRightMostChild(newNode); + } else { + newNode = node.out(); + } + if (isValid(newNode)) { + return newNode; + } else if (newNode) { + return this.getPreviousNode(newNode, isValid); + } + return null; + } + + /** + * From the given node find either the next valid sibling or the parent's + * next sibling. + * + * @param node The current position in the AST. + * @returns The next sibling node, the parent's next sibling, or null. + */ + private findSiblingOrParentSibling(node: ASTNode | null): ASTNode | null { + if (!node) { + return null; + } + const nextNode = node.next(); + if (nextNode) { + return nextNode; + } + return this.findSiblingOrParentSibling(node.out()); + } + + /** + * Get the right most child of a node. + * + * @param node The node to find the right most child of. + * @returns The right most child of the given node, or the node if no child + * exists. + */ + private getRightMostChild(node: ASTNode): ASTNode | null { + let newNode = node.in(); + if (!newNode) { + return node; + } + for ( + let nextNode: ASTNode | null = newNode; + nextNode; + nextNode = newNode.next() + ) { + newNode = nextNode; + } + return this.getRightMostChild(newNode); + } + + /** + * Prepare for the deletion of a block by making a list of nodes we + * could move the cursor to afterwards and save it to + * this.potentialNodes. + * + * After the deletion has occurred, call postDelete to move it to + * the first valid node on that list. + * + * The locations to try (in order of preference) are: + * + * - The current location. + * - The connection to which the deleted block is attached. + * - The block connected to the next connection of the deleted block. + * - The parent block of the deleted block. + * - A location on the workspace beneath the deleted block. + * + * N.B.: When block is deleted, all of the blocks conneccted to that + * block's inputs are also deleted, but not blocks connected to its + * next connection. + * + * @param deletedBlock The block that is being deleted. + */ + preDelete(deletedBlock: Block) { + const curNode = this.getCurNode(); + + const nodes: ASTNode[] = curNode ? [curNode] : []; + // The connection to which the deleted block is attached. + const parentConnection = + deletedBlock.previousConnection?.targetConnection ?? + deletedBlock.outputConnection?.targetConnection; + if (parentConnection) { + const parentNode = ASTNode.createConnectionNode(parentConnection); + if (parentNode) nodes.push(parentNode); + } + // The block connected to the next connection of the deleted block. + const nextBlock = deletedBlock.getNextBlock(); + if (nextBlock) { + const nextNode = ASTNode.createBlockNode(nextBlock); + if (nextNode) nodes.push(nextNode); + } + // The parent block of the deleted block. + const parentBlock = deletedBlock.getParent(); + if (parentBlock) { + const parentNode = ASTNode.createBlockNode(parentBlock); + if (parentNode) nodes.push(parentNode); + } + // A location on the workspace beneath the deleted block. + // Move to the workspace. + const curBlock = curNode?.getSourceBlock(); + if (curBlock) { + const workspaceNode = ASTNode.createWorkspaceNode( + this.workspace, + curBlock.getRelativeToSurfaceXY(), + ); + if (workspaceNode) nodes.push(workspaceNode); + } + this.potentialNodes = nodes; + } + + /** + * Move the cursor to the first valid location in + * this.potentialNodes, following a block deletion. + */ + postDelete() { + const nodes = this.potentialNodes; + this.potentialNodes = null; + if (!nodes) throw new Error('must call preDelete first'); + for (const node of nodes) { + if (this.validNode(node) && !node.getSourceBlock()?.disposed) { + this.setCurNode(node); + return; + } + } + throw new Error('no valid nodes in this.potentialNodes'); + } + + /** + * Get the current location of the cursor. + * + * Overrides normal Marker getCurNode to update the current node from the + * selected block. This typically happens via the selection listener but that + * is not called immediately when `Gesture` calls + * `Blockly.common.setSelected`. In particular the listener runs after showing + * the context menu. + * + * @returns The current field, connection, or block the cursor is on. + */ + override getCurNode(): ASTNode | null { + this.updateCurNodeFromSelection(); + return super.getCurNode(); + } + + /** + * 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: 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. + * + * Overrides normal Marker setCurNode logic to call + * this.drawMarker() instead of this.drawer.draw() directly. + * + * @param newNode The new location of the cursor. + * @param updateSelection If true (the default) we'll update the selection + * too. + */ + override setCurNode(newNode: ASTNode | null, updateSelection = true) { + if (updateSelection) { + this.updateSelectionFromNode(newNode); + } + + super.setCurNode(newNode); + + // Try to scroll cursor into view. + if (newNode?.getType() === ASTNode.types.BLOCK) { + const block = newNode.getLocation() as BlockSvg; + block.workspace.scrollBoundsIntoView( + block.getBoundingRectangleWithoutChildren(), + ); + } + } + + /** + * Draw this cursor's marker. + * + * This is a wrapper around this.drawer.draw (usually implemented by + * MarkerSvg.prototype.draw) that will, if newNode is a BLOCK node, + * instead call `setSelected` to select it (if it's a regular block) + * or `addSelect` (if it's a shadow block, since shadow blocks can't + * be selected) instead of using the normal drawer logic. + * + * TODO(#142): The selection and fake-selection code was originally + * a hack added for testing on October 28 2024, because the default + * drawer (MarkerSvg) behaviour in Zelos was to draw a box around + * the block and all attached child blocks, which was confusing when + * navigating stacks. + * + * Since then we have decided that we probably _do_ in most cases + * want navigating to a block to select the block, but more + * particularly that we want navigation to move _focus_. Replace + * this selection hack with non-hacky changing of focus once that's + * possible. + * + * @param oldNode The previous node. + * @param curNode The current node. + * @param realDrawer The object ~in charge of drawing the marker. + */ + private drawMarker( + oldNode: ASTNode | null, + curNode: ASTNode | null, + realDrawer: MarkerSvg, + ) { + // If old node was a block, unselect it or remove fake selection. + if (oldNode?.getType() === ASTNode.types.BLOCK) { + const block = oldNode.getLocation() as BlockSvg; + if (!block.isShadow()) { + // Selection should already be in sync. + } else { + block.removeSelect(); + } + } + + if (this.isZelos && oldNode && this.isValueInputConnection(oldNode)) { + this.hideAtInput(oldNode); + } + + const curNodeType = curNode?.getType(); + const isZelosInputConnection = + this.isZelos && curNode && this.isValueInputConnection(curNode); + + // If drawing can't be handled locally, just use the drawer. + if (curNodeType !== ASTNode.types.BLOCK && !isZelosInputConnection) { + realDrawer.draw(oldNode, curNode); + return; + } + + // Hide any visible marker SVG and instead do some manual rendering. + realDrawer.hide(); + + if (isZelosInputConnection) { + this.showAtInput(curNode); + } else if (curNode && curNodeType === ASTNode.types.BLOCK) { + const block = curNode.getLocation() as BlockSvg; + if (!block.isShadow()) { + // Selection should already be in sync. + } else { + block.addSelect(); + block.getParent()?.removeSelect(); + } + } + + // Call MarkerSvg.prototype.fireMarkerEvent like + // MarkerSvg.prototype.draw would (even though it's private). + (realDrawer as any)?.fireMarkerEvent?.(oldNode, curNode); + } + + /** + * Check whether the node represents a value input connection. + * + * @param node The node to check + * @returns True if the node represents a value input connection. + */ + private isValueInputConnection(node: ASTNode) { + if (node?.getType() !== ASTNode.types.INPUT) return false; + const connection = node.getLocation() as Connection; + return connection.type === ConnectionType.INPUT_VALUE; + } + + /** + * Hide the cursor rendering at the given input node. + * + * @param node The input node to hide. + */ + private hideAtInput(node: ASTNode) { + const inputConnection = node.getLocation() as Connection; + const sourceBlock = inputConnection.getSourceBlock() as BlockSvg; + const input = inputConnection.getParentInput(); + if (input) { + const pathObject = sourceBlock.pathObject as PathObject; + const outlinePath = pathObject.getOutlinePath(input.name); + dom.removeClass(outlinePath, 'inputActiveFocus'); + } + } + + /** + * Show the cursor rendering at the given input node. + * + * @param node The input node to show. + */ + private showAtInput(node: ASTNode) { + const inputConnection = node.getLocation() as Connection; + const sourceBlock = inputConnection.getSourceBlock() as BlockSvg; + const input = inputConnection.getParentInput(); + if (input) { + const pathObject = sourceBlock.pathObject as PathObject; + const outlinePath = pathObject.getOutlinePath(input.name); + dom.addClass(outlinePath, 'inputActiveFocus'); + } + } + + /** + * Event listener that syncs the cursor location to the selected block on + * SELECTED events. + * + * This does not run early enough in all cases so `getCurNode()` also updates + * the node from the selection. + * + * @param event The `Selected` event. + */ + private changeListener(event: Abstract) { + switch (event.type) { + case EventType.SELECTED: + this.updateCurNodeFromSelection(); + break; + case EventType.CLICK: { + const click = event as Click; + if ( + click.workspaceId === this.workspace.id && + click.targetType === ClickTarget.WORKSPACE + ) { + this.setCurNode(null); + } + } + } + } + + /** + * Updates the current node to match the selection. + * + * Clears the current node if it's on a block but the selection is null. + * Sets the node to a block if selected for our workspace. + * For shadow blocks selections the parent is used by default (unless we're + * already on the shadow block via keyboard) as that's where the visual + * selection is. + */ + private updateCurNodeFromSelection() { + const curNode = super.getCurNode(); + const selected = common.getSelected(); + + if (selected === null && curNode?.getType() === ASTNode.types.BLOCK) { + this.setCurNode(null, false); + return; + } + if (selected?.workspace !== this.workspace) { + return; + } + if (selected instanceof BlockSvg) { + let block: BlockSvg | null = selected; + if (selected.isShadow()) { + // OK if the current node is on the parent OR the shadow block. + // The former happens for clicks, the latter for keyboard nav. + if ( + curNode && + (curNode.getLocation() === block || + curNode.getLocation() === block.getParent()) + ) { + return; + } + block = block.getParent(); + } + if (block) { + this.setCurNode(ASTNode.createBlockNode(block), false); + } + } + } + + /** + * Updates the selection from the node. + * + * Clears the selection for non-block nodes. + * Clears the selection for shadow blocks as the selection is drawn on + * the parent but the cursor will be drawn on the shadow block itself. + * We need to take care not to later clear the current node due to that null + * selection, so we track the latest selection we're in sync with. + * + * @param newNode The new node. + */ + private updateSelectionFromNode(newNode: ASTNode | null) { + if (newNode?.getType() === ASTNode.types.BLOCK) { + if (common.getSelected() !== newNode.getLocation()) { + eventUtils.disable(); + common.setSelected(newNode.getLocation() as BlockSvg); + eventUtils.enable(); + } + } else { + if (common.getSelected()) { + eventUtils.disable(); + common.setSelected(null); + eventUtils.enable(); + } + } + } +} + +registry.register(registry.Type.CURSOR, registry.DEFAULT, LineCursor); diff --git a/core/marker_manager.ts b/core/marker_manager.ts index 51183242d3d..a8d1d20c2bd 100644 --- a/core/marker_manager.ts +++ b/core/marker_manager.ts @@ -11,7 +11,7 @@ */ // Former goog.module ID: Blockly.MarkerManager -import type {Cursor} from './keyboard_nav/cursor.js'; +import type {LineCursor} from './keyboard_nav/line_cursor.js'; import type {Marker} from './keyboard_nav/marker.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -23,7 +23,7 @@ export class MarkerManager { static readonly LOCAL_MARKER = 'local_marker_1'; /** The cursor. */ - private cursor: Cursor | null = null; + private cursor: LineCursor | null = null; /** The cursor's SVG element. */ private cursorSvg: SVGElement | null = null; @@ -83,7 +83,7 @@ export class MarkerManager { * * @returns The cursor for this workspace. */ - getCursor(): Cursor | null { + getCursor(): LineCursor | null { return this.cursor; } @@ -104,7 +104,7 @@ export class MarkerManager { * * @param cursor The cursor used to move around this workspace. */ - setCursor(cursor: Cursor) { + setCursor(cursor: LineCursor) { this.cursor?.getDrawer()?.dispose(); this.cursor = cursor; if (this.cursor) { diff --git a/core/registry.ts b/core/registry.ts index e026514f1c9..2b00b775dea 100644 --- a/core/registry.ts +++ b/core/registry.ts @@ -26,7 +26,7 @@ import type { IVariableModelStatic, IVariableState, } from './interfaces/i_variable_model.js'; -import type {Cursor} from './keyboard_nav/cursor.js'; +import type {LineCursor} from './keyboard_nav/line_cursor.js'; import type {Options} from './options.js'; import type {Renderer} from './renderers/common/renderer.js'; import type {Theme} from './theme.js'; @@ -78,7 +78,7 @@ export class Type<_T> { 'connectionPreviewer', ); - static CURSOR = new Type('cursor'); + static CURSOR = new Type('cursor'); static EVENT = new Type('event'); diff --git a/core/renderers/zelos/path_object.ts b/core/renderers/zelos/path_object.ts index cf6fff7e8c2..f40426483a7 100644 --- a/core/renderers/zelos/path_object.ts +++ b/core/renderers/zelos/path_object.ts @@ -161,10 +161,11 @@ export class PathObject extends BasePathObject { /** * Create's an outline path for the specified input. * + * @internal * @param name The input name. * @returns The SVG outline path. */ - private getOutlinePath(name: string): SVGElement { + getOutlinePath(name: string): SVGElement { if (!this.outlines.has(name)) { this.outlines.set( name, diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 78506115e88..68a1bd939fd 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -49,7 +49,7 @@ import type { IVariableModel, IVariableState, } from './interfaces/i_variable_model.js'; -import type {Cursor} from './keyboard_nav/cursor.js'; +import type {LineCursor} from './keyboard_nav/line_cursor.js'; import type {Marker} from './keyboard_nav/marker.js'; import {LayerManager} from './layer_manager.js'; import {MarkerManager} from './marker_manager.js'; @@ -487,7 +487,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * * @returns The cursor for the workspace. */ - getCursor(): Cursor | null { + getCursor(): LineCursor | null { if (this.markerManager) { return this.markerManager.getCursor(); } @@ -828,7 +828,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { this.options, ); - if (CursorClass) this.markerManager.setCursor(new CursorClass()); + if (CursorClass) this.markerManager.setCursor(new CursorClass(this)); const isParentWorkspace = this.options.parentWorkspace === null; this.renderer.createDom( @@ -2541,6 +2541,71 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { this.removeClass('blocklyReadOnly'); } } + + /** + * Scrolls the provided bounds into view. + * + * In the case of small workspaces/large bounds, this function prioritizes + * getting the top left corner of the bounds into view. It also adds some + * padding around the bounds to allow the element to be comfortably in view. + * + * @internal + * @param bounds A rectangle to scroll into view, as best as possible. + * @param padding Amount of spacing to put between the bounds and the edge of + * the workspace's viewport. + */ + scrollBoundsIntoView(bounds: Rect, padding = 10) { + if (Gesture.inProgress()) { + // This can cause jumps during a drag and is only suited for keyboard nav. + return; + } + const scale = this.getScale(); + + const rawViewport = this.getMetricsManager().getViewMetrics(true); + const viewport = new Rect( + rawViewport.top, + rawViewport.top + rawViewport.height, + rawViewport.left, + rawViewport.left + rawViewport.width, + ); + + if ( + bounds.left >= viewport.left && + bounds.top >= viewport.top && + bounds.right <= viewport.right && + bounds.bottom <= viewport.bottom + ) { + // Do nothing if the block is fully inside the viewport. + return; + } + + // Add some padding to the bounds so the element is scrolled comfortably + // into view. + bounds = bounds.clone(); + bounds.top -= padding; + bounds.bottom += padding; + bounds.left -= padding; + bounds.right += padding; + + let deltaX = 0; + let deltaY = 0; + + if (bounds.left < viewport.left) { + deltaX = viewport.left - bounds.left; + } else if (bounds.right > viewport.right) { + deltaX = viewport.right - bounds.right; + } + + if (bounds.top < viewport.top) { + deltaY = viewport.top - bounds.top; + } else if (bounds.bottom > viewport.bottom) { + deltaY = viewport.bottom - bounds.bottom; + } + + deltaX *= scale; + deltaY *= scale; + this.scroll(this.scrollX + deltaX, this.scrollY + deltaY); + } } /** diff --git a/tests/mocha/cursor_test.js b/tests/mocha/cursor_test.js index bb5026d7ac3..3242edd2a37 100644 --- a/tests/mocha/cursor_test.js +++ b/tests/mocha/cursor_test.js @@ -21,21 +21,21 @@ suite('Cursor', function () { 'args0': [ { 'type': 'field_input', - 'name': 'NAME', + 'name': 'NAME1', 'text': 'default', }, { 'type': 'field_input', - 'name': 'NAME', + 'name': 'NAME2', 'text': 'default', }, { 'type': 'input_value', - 'name': 'NAME', + 'name': 'NAME3', }, { 'type': 'input_statement', - 'name': 'NAME', + 'name': 'NAME4', }, ], 'previousStatement': null, @@ -84,23 +84,24 @@ suite('Cursor', function () { sharedTestTeardown.call(this); }); - test('Next - From a Previous skip over next connection and block', function () { + test('Next - From a Previous connection go to the next block', function () { const prevNode = ASTNode.createConnectionNode( this.blocks.A.previousConnection, ); this.cursor.setCurNode(prevNode); this.cursor.next(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.B.previousConnection); + assert.equal(curNode.getLocation(), this.blocks.A); }); - test('Next - From last block in a stack go to next connection', function () { - const prevNode = ASTNode.createConnectionNode( - this.blocks.B.previousConnection, - ); + test('Next - From a block go to its statement input', function () { + const prevNode = ASTNode.createBlockNode(this.blocks.B); this.cursor.setCurNode(prevNode); this.cursor.next(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.B.nextConnection); + assert.equal( + curNode.getLocation(), + this.blocks.B.getInput('NAME4').connection, + ); }); test('In - From output connection', function () { @@ -111,24 +112,24 @@ suite('Cursor', function () { this.cursor.setCurNode(outputNode); this.cursor.in(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), fieldBlock.inputList[0].fieldRow[0]); + assert.equal(curNode.getLocation(), fieldBlock); }); - test('Prev - From previous connection skip over next connection', function () { + test('Prev - From previous connection does not skip over next connection', function () { const prevConnection = this.blocks.B.previousConnection; const prevConnectionNode = ASTNode.createConnectionNode(prevConnection); this.cursor.setCurNode(prevConnectionNode); this.cursor.prev(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.A.previousConnection); + assert.equal(curNode.getLocation(), this.blocks.A.nextConnection); }); - test('Out - From field skip over block node', function () { + test('Out - From field does not skip over block node', function () { const field = this.blocks.E.inputList[0].fieldRow[0]; const fieldNode = ASTNode.createFieldNode(field); this.cursor.setCurNode(fieldNode); this.cursor.out(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.E.outputConnection); + assert.equal(curNode.getLocation(), this.blocks.E); }); }); From 902b26b1a1b7552652cc46a41d73b1a6d424b638 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 3 Apr 2025 22:25:50 +0000 Subject: [PATCH 118/222] chore: part 1 of addressing reviewer comments. --- core/blockly.ts | 3 +- core/focus_manager.ts | 51 +- core/utils/focusable_tree_traverser.ts | 22 +- tests/mocha/focus_manager_test.js | 1695 +++++++++--------- tests/mocha/focusable_tree_traverser_test.js | 181 +- 5 files changed, 997 insertions(+), 955 deletions(-) diff --git a/core/blockly.ts b/core/blockly.ts index c29961f591f..a98f0f695bb 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -106,7 +106,7 @@ import {FlyoutItem} from './flyout_item.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import {FlyoutSeparator} from './flyout_separator.js'; import {VerticalFlyout} from './flyout_vertical.js'; -import {FocusManager, getFocusManager} from './focus_manager.js'; +import {FocusManager, getFocusManager, ReturnEphemeralFocus} from './focus_manager.js'; import {CodeGenerator} from './generator.js'; import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; @@ -523,6 +523,7 @@ export { FlyoutMetricsManager, FlyoutSeparator, FocusManager, + ReturnEphemeralFocus, CodeGenerator as Generator, Gesture, Grid, diff --git a/core/focus_manager.ts b/core/focus_manager.ts index d79db4b4bcd..f0cc32b7402 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -30,6 +30,9 @@ export type ReturnEphemeralFocus = () => void; * focusNode(). */ export class FocusManager { + static readonly ACTIVE_FOCUS_NODE_CSS_CLASS_NAME = 'blocklyActiveFocus'; + static readonly PASSIVE_FOCUS_NODE_CSS_CLASS_NAME = 'blocklyPassiveFocus'; + focusedNode: IFocusableNode | null = null; registeredTrees: Array = []; @@ -45,7 +48,7 @@ export class FocusManager { // The target that now has focus. const activeElement = document.activeElement; - let newNode: IFocusableNode | null = null; + let newNode: IFocusableNode | null | undefined = null; if ( activeElement instanceof HTMLElement || activeElement instanceof SVGElement @@ -53,10 +56,10 @@ export class FocusManager { // If the target losing focus maps to any tree, then it should be // updated. Per the contract of findFocusableNodeFor only one tree // should claim the element. - const matchingNodes = this.registeredTrees.map((tree) => - tree.findFocusableNodeFor(activeElement), - ); - newNode = matchingNodes.find((node) => !!node) ?? null; + for (const tree of this.registeredTrees) { + newNode = tree.findFocusableNodeFor(activeElement); + if (newNode) break; + } } if (newNode) { @@ -91,7 +94,7 @@ export class FocusManager { * unregisterTree. */ isRegistered(tree: IFocusableTree): boolean { - return this.registeredTrees.findIndex((reg) => reg == tree) !== -1; + return this.registeredTrees.findIndex((reg) => reg === tree) !== -1; } /** @@ -107,13 +110,13 @@ export class FocusManager { if (!this.isRegistered(tree)) { throw Error(`Attempted to unregister not registered tree: ${tree}.`); } - const treeIndex = this.registeredTrees.findIndex((tree) => tree == tree); + const treeIndex = this.registeredTrees.findIndex((tree) => tree === tree); this.registeredTrees.splice(treeIndex, 1); const focusedNode = tree.getFocusedNode(); const root = tree.getRootFocusableNode(); - if (focusedNode != null) this.removeHighlight(focusedNode); - if (this.focusedNode == focusedNode || this.focusedNode == root) { + if (focusedNode) this.removeHighlight(focusedNode); + if (this.focusedNode === focusedNode || this.focusedNode === root) { this.focusedNode = null; } this.removeHighlight(root); @@ -181,25 +184,25 @@ export class FocusManager { * focus. */ focusNode(focusableNode: IFocusableNode): void { - const curTree = focusableNode.getFocusableTree(); - if (!this.isRegistered(curTree)) { + const nextTree = focusableNode.getFocusableTree(); + if (!this.isRegistered(nextTree)) { throw Error(`Attempted to focus unregistered node: ${focusableNode}.`); } const prevNode = this.focusedNode; - if (prevNode && prevNode.getFocusableTree() !== curTree) { + if (prevNode && prevNode.getFocusableTree() !== nextTree) { this.setNodeToPassive(prevNode); } // If there's a focused node in the new node's tree, ensure it's reset. - const prevNodeCurTree = curTree.getFocusedNode(); - const curTreeRoot = curTree.getRootFocusableNode(); - if (prevNodeCurTree) { - this.removeHighlight(prevNodeCurTree); + const prevNodeNextTree = nextTree.getFocusedNode(); + const nextTreeRoot = nextTree.getRootFocusableNode(); + if (prevNodeNextTree) { + this.removeHighlight(prevNodeNextTree); } // For caution, ensure that the root is always reset since getFocusedNode() // is expected to return null if the root was highlighted, if the root is // not the node now being set to active. - if (curTreeRoot !== focusableNode) { - this.removeHighlight(curTreeRoot); + if (nextTreeRoot !== focusableNode) { + this.removeHighlight(nextTreeRoot); } if (!this.currentlyHoldsEphemeralFocus) { // Only change the actively focused node if ephemeral state isn't held. @@ -271,21 +274,21 @@ export class FocusManager { private setNodeToActive(node: IFocusableNode): void { const element = node.getFocusableElement(); - dom.addClass(element, 'blocklyActiveFocus'); - dom.removeClass(element, 'blocklyPassiveFocus'); + dom.addClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + dom.removeClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); element.focus(); } private setNodeToPassive(node: IFocusableNode): void { const element = node.getFocusableElement(); - dom.removeClass(element, 'blocklyActiveFocus'); - dom.addClass(element, 'blocklyPassiveFocus'); + dom.removeClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + dom.addClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); } private removeHighlight(node: IFocusableNode): void { const element = node.getFocusableElement(); - dom.removeClass(element, 'blocklyActiveFocus'); - dom.removeClass(element, 'blocklyPassiveFocus'); + dom.removeClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + dom.removeClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); } } diff --git a/core/utils/focusable_tree_traverser.ts b/core/utils/focusable_tree_traverser.ts index eb6de1e0596..8061e981b50 100644 --- a/core/utils/focusable_tree_traverser.ts +++ b/core/utils/focusable_tree_traverser.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {FocusManager} from '../focus_manager.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import * as dom from '../utils/dom.js'; @@ -13,6 +14,9 @@ import * as dom from '../utils/dom.js'; * tree traversals. */ export class FocusableTreeTraverser { + static readonly ACTIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME}`; + static readonly PASSIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME}`; + /** * Returns the current IFocusableNode that either has the CSS class * 'blocklyActiveFocus' or 'blocklyPassiveFocus', only considering HTML and @@ -26,28 +30,28 @@ export class FocusableTreeTraverser { static findFocusedNode(tree: IFocusableTree): IFocusableNode | null { const root = tree.getRootFocusableNode().getFocusableElement(); if ( - dom.hasClass(root, 'blocklyActiveFocus') || - dom.hasClass(root, 'blocklyPassiveFocus') + dom.hasClass(root, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME) || + dom.hasClass(root, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME) ) { // The root has focus. return tree.getRootFocusableNode(); } - const activeEl = root.querySelector('.blocklyActiveFocus'); - let active: IFocusableNode | null = null; + const activeEl = root.querySelector(this.ACTIVE_FOCUS_NODE_CSS_SELECTOR); if (activeEl instanceof HTMLElement || activeEl instanceof SVGElement) { - active = tree.findFocusableNodeFor(activeEl); + const active = tree.findFocusableNodeFor(activeEl); + if (active) return active; } // At most there should be one passive indicator per tree (not considering // subtrees). - const passiveEl = root.querySelector('.blocklyPassiveFocus'); - let passive: IFocusableNode | null = null; + const passiveEl = root.querySelector(this.PASSIVE_FOCUS_NODE_CSS_SELECTOR); if (passiveEl instanceof HTMLElement || passiveEl instanceof SVGElement) { - passive = tree.findFocusableNodeFor(passiveEl); + const passive = tree.findFocusableNodeFor(passiveEl); + if (passive) return passive; } - return active ?? passive; + return null; } /** diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js index e18dbc79e67..fa8f12083f6 100644 --- a/tests/mocha/focus_manager_test.js +++ b/tests/mocha/focus_manager_test.js @@ -15,6 +15,55 @@ import { sharedTestTeardown, } from './test_helpers/setup_teardown.js'; +class FocusableNodeImpl { + constructor(element, tree) { + this.element = element; + this.tree = tree; + } + + getFocusableElement() { + return this.element; + } + + getFocusableTree() { + return this.tree; + } +} + +class FocusableTreeImpl { + constructor(rootElement, nestedTrees) { + this.nestedTrees = nestedTrees; + this.idToNodeMap = {}; + this.rootNode = this.addNode(rootElement); + } + + addNode(element) { + const node = new FocusableNodeImpl(element, this); + this.idToNodeMap[element.id] = node; + return node; + } + + getFocusedNode() { + return FocusableTreeTraverser.findFocusedNode(this); + } + + getRootFocusableNode() { + return this.rootNode; + } + + getNestedTrees() { + return this.nestedTrees; + } + + lookUpFocusableNode(id) { + return this.idToNodeMap[id]; + } + + findFocusableNodeFor(element) { + return FocusableTreeTraverser.findFocusableNodeFor(element, this); + } +} + suite('FocusManager', function () { setup(function () { sharedTestSetup.call(this); @@ -27,47 +76,6 @@ suite('FocusManager', function () { }; this.focusManager = new FocusManager(addDocumentEventListener); - const FocusableNodeImpl = function (element, tree) { - this.getFocusableElement = function () { - return element; - }; - - this.getFocusableTree = function () { - return tree; - }; - }; - const FocusableTreeImpl = function (rootElement, nestedTrees) { - this.idToNodeMap = {}; - - this.addNode = function (element) { - const node = new FocusableNodeImpl(element, this); - this.idToNodeMap[element.id] = node; - return node; - }; - - this.getFocusedNode = function () { - return FocusableTreeTraverser.findFocusedNode(this); - }; - - this.getRootFocusableNode = function () { - return this.rootNode; - }; - - this.getNestedTrees = function () { - return nestedTrees; - }; - - this.lookUpFocusableNode = function (id) { - return this.idToNodeMap[id]; - }; - - this.findFocusableNodeFor = function (element) { - return FocusableTreeTraverser.findFocusableNodeFor(element, this); - }; - - this.rootNode = this.addNode(rootElement); - }; - const createFocusableTree = function (rootElementId, nestedTrees) { return new FocusableTreeImpl( document.getElementById(rootElementId), @@ -153,7 +161,7 @@ suite('FocusManager', function () { document.removeEventListener(eventType, eventListener); const removeFocusIndicators = function (element) { - element.classList.remove('blocklyActiveFocus', 'blocklyPassiveFocus'); + element.classList.remove(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }; // Ensure all node CSS styles are reset so that state isn't leaked between tests. @@ -186,6 +194,14 @@ suite('FocusManager', function () { document.body.focus(); }); + assert.includesClass = function(classList, className) { + assert.isTrue(classList.contains(className), 'Expected class list to include: ' + className); + }; + + assert.notIncludesClass = function(classList, className) { + assert.isFalse(classList.contains(className), 'Expected class list to not include: ' + className); + }; + /* Basic lifecycle tests. */ suite('registerTree()', function () { @@ -371,7 +387,7 @@ suite('FocusManager', function () { document.removeEventListener('focusin', focusListener); // There should be exactly 1 focus event fired from focusNode(). - assert.equal(focusCount, 1); + assert.strictEqual(focusCount, 1); }); }); @@ -399,7 +415,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableTree1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree1, ); @@ -411,7 +427,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableTree1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree1, ); @@ -424,7 +440,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableTree2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree2, ); @@ -437,7 +453,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableTree2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree2, ); @@ -450,7 +466,7 @@ suite('FocusManager', function () { this.testFocusableTree1.getRootFocusableNode(), ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree1, ); @@ -461,7 +477,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree1Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree1, ); @@ -472,7 +488,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree1Node1Child1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree1, ); @@ -484,7 +500,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree1Node2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree1, ); @@ -497,7 +513,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree2Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree2, ); @@ -512,7 +528,7 @@ suite('FocusManager', function () { this.testFocusableTree2.getRootFocusableNode(), ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree2, ); @@ -557,7 +573,7 @@ suite('FocusManager', function () { this.focusManager.unregisterTree(this.testFocusableTree1); // Since the most recent tree still exists, it still has focus. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree2, ); @@ -569,7 +585,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableNestedTree4); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedTree4, ); @@ -581,7 +597,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableNestedTree4Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedTree4, ); @@ -594,7 +610,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableNestedTree4Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedTree4, ); @@ -606,7 +622,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableTree1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree1.getRootFocusableNode(), ); @@ -620,7 +636,7 @@ suite('FocusManager', function () { // The original node retains focus since the tree already holds focus (per focusTree's // contract). - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree1Node1, ); @@ -633,7 +649,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableTree2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2.getRootFocusableNode(), ); @@ -646,7 +662,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableTree2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2.getRootFocusableNode(), ); @@ -659,7 +675,7 @@ suite('FocusManager', function () { this.testFocusableTree1.getRootFocusableNode(), ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree1.getRootFocusableNode(), ); @@ -670,7 +686,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree1Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree1Node1, ); @@ -681,7 +697,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree1Node1Child1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree1Node1Child1, ); @@ -693,7 +709,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree1Node2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree1Node2, ); @@ -706,7 +722,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree2Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); @@ -721,7 +737,7 @@ suite('FocusManager', function () { this.testFocusableTree2.getRootFocusableNode(), ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2.getRootFocusableNode(), ); @@ -766,7 +782,7 @@ suite('FocusManager', function () { this.focusManager.unregisterTree(this.testFocusableTree1); // Since the most recent tree still exists, it still has focus. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); @@ -778,7 +794,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableNestedTree4); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedTree4.getRootFocusableNode(), ); @@ -790,7 +806,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableNestedTree4Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedTree4Node1, ); @@ -803,7 +819,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableNestedTree4Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedTree4Node1, ); @@ -818,10 +834,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -834,10 +850,10 @@ suite('FocusManager', function () { // The original node retains active focus since the tree already holds focus (per // focusTree's contract). const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -851,10 +867,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree2 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -868,10 +884,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree2 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -885,10 +901,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -898,10 +914,10 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree1Node1); const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -912,13 +928,13 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree1Node2); const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -929,10 +945,10 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree1Node2); const newNodeElem = this.testFocusableTree1Node2.getFocusableElement(); - assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(newNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -944,13 +960,13 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree2Node1); const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(prevNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -962,10 +978,10 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree2Node1); const newNodeElem = this.testFocusableTree2Node1.getFocusableElement(); - assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(newNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -981,10 +997,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree2 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -998,10 +1014,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1013,10 +1029,10 @@ suite('FocusManager', function () { // Since the tree was unregistered it no longer has focus indicators. const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1034,21 +1050,21 @@ suite('FocusManager', function () { this.testFocusableTree2Node1.getFocusableElement(); const removedNodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.include( - Array.from(otherNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(otherNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1066,21 +1082,21 @@ suite('FocusManager', function () { this.testFocusableTree2Node1.getFocusableElement(); const removedNodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.include( - Array.from(otherNodeElem.classList), - 'blocklyActiveFocus', + assert.includesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(otherNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1096,8 +1112,8 @@ suite('FocusManager', function () { // passive now that the new node is active. const node1 = this.testFocusableTree1Node1.getFocusableElement(); const node2 = this.testFocusableTree1Node2.getFocusableElement(); - assert.notInclude(Array.from(node1.classList), 'blocklyPassiveFocus'); - assert.notInclude(Array.from(node2.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(node1.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass(node2.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('registered tree focusTree()ed other tree node passively focused tree node now has active property', function () { @@ -1114,15 +1130,15 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1138,15 +1154,15 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1159,10 +1175,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableNestedTree4 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1174,10 +1190,10 @@ suite('FocusManager', function () { const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1191,21 +1207,21 @@ suite('FocusManager', function () { const prevNodeElem = this.testFocusableTree2Node1.getFocusableElement(); const currNodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(prevNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(currNodeElem.classList), - 'blocklyActiveFocus', + assert.includesClass( + currNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(currNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + currNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); }); @@ -1218,7 +1234,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree1, ); @@ -1229,7 +1245,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree1, ); @@ -1240,7 +1256,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node1.child1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree1, ); @@ -1252,7 +1268,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node2').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree1, ); @@ -1265,7 +1281,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree2.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree2, ); @@ -1278,7 +1294,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree2').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree2, ); @@ -1292,7 +1308,7 @@ suite('FocusManager', function () { .focus(); // The tree of the unregistered child element should take focus. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree1, ); @@ -1325,7 +1341,7 @@ suite('FocusManager', function () { document.getElementById('testUnfocusableElement').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree1, ); @@ -1370,7 +1386,7 @@ suite('FocusManager', function () { this.focusManager.unregisterTree(this.testFocusableTree1); // Since the most recent tree still exists, it still has focus. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree2, ); @@ -1397,7 +1413,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedTree4').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedTree4, ); @@ -1409,7 +1425,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedTree4.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedTree4, ); @@ -1422,7 +1438,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedTree4.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedTree4, ); @@ -1434,7 +1450,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree1.getRootFocusableNode(), ); @@ -1445,7 +1461,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree1Node1, ); @@ -1456,7 +1472,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node1.child1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree1Node1Child1, ); @@ -1468,7 +1484,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node2').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree1Node2, ); @@ -1481,7 +1497,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree2.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); @@ -1494,7 +1510,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree2').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2.getRootFocusableNode(), ); @@ -1508,7 +1524,7 @@ suite('FocusManager', function () { .focus(); // The nearest node of the unregistered child element should take focus. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree1Node2, ); @@ -1535,13 +1551,13 @@ suite('FocusManager', function () { assert.isNull(this.focusManager.getFocusedNode()); }); - test('unfocuasble element focus()ed after registered node focused returns original node', function () { + test('unfocusable element focus()ed after registered node focused returns original node', function () { this.focusManager.registerTree(this.testFocusableTree1); document.getElementById('testFocusableTree1.node1').focus(); document.getElementById('testUnfocusableElement').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree1Node1, ); @@ -1586,7 +1602,7 @@ suite('FocusManager', function () { this.focusManager.unregisterTree(this.testFocusableTree1); // Since the most recent tree still exists, it still has focus. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); @@ -1613,7 +1629,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedTree4').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedTree4.getRootFocusableNode(), ); @@ -1625,7 +1641,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedTree4.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedTree4Node1, ); @@ -1638,7 +1654,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedTree4.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedTree4Node1, ); @@ -1653,10 +1669,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1666,10 +1682,10 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node1').focus(); const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1680,13 +1696,13 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node2').focus(); const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1697,10 +1713,10 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node2').focus(); const newNodeElem = this.testFocusableTree1Node2.getFocusableElement(); - assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(newNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1712,13 +1728,13 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree2.node1').focus(); const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(prevNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1730,10 +1746,10 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree2.node1').focus(); const newNodeElem = this.testFocusableTree2Node1.getFocusableElement(); - assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(newNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1747,10 +1763,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree2 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1763,10 +1779,10 @@ suite('FocusManager', function () { // The nearest node of the unregistered child element should be actively focused. const nodeElem = this.testFocusableTree1Node2.getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1778,10 +1794,10 @@ suite('FocusManager', function () { const rootElem = document.getElementById( 'testUnregisteredFocusableTree3', ); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1793,10 +1809,10 @@ suite('FocusManager', function () { const nodeElem = document.getElementById( 'testUnregisteredFocusableTree3.node1', ); - assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1812,18 +1828,18 @@ suite('FocusManager', function () { const attemptedNewNodeElem = document.getElementById( 'testUnfocusableElement', ); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(attemptedNewNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + attemptedNewNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(attemptedNewNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + attemptedNewNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1837,10 +1853,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1852,10 +1868,10 @@ suite('FocusManager', function () { // Since the tree was unregistered it no longer has focus indicators. const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1873,21 +1889,21 @@ suite('FocusManager', function () { this.testFocusableTree2Node1.getFocusableElement(); const removedNodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.include( - Array.from(otherNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(otherNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1905,21 +1921,21 @@ suite('FocusManager', function () { this.testFocusableTree2Node1.getFocusableElement(); const removedNodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.include( - Array.from(otherNodeElem.classList), - 'blocklyActiveFocus', + assert.includesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(otherNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1937,21 +1953,21 @@ suite('FocusManager', function () { this.testFocusableTree2Node1.getFocusableElement(); const removedNodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.notInclude( - Array.from(otherNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(otherNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -1967,8 +1983,8 @@ suite('FocusManager', function () { // passive now that the new node is active. const node1 = this.testFocusableTree1Node1.getFocusableElement(); const node2 = this.testFocusableTree1Node2.getFocusableElement(); - assert.notInclude(Array.from(node1.classList), 'blocklyPassiveFocus'); - assert.notInclude(Array.from(node2.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(node1.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass(node2.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('registered tree focus()ed other tree node passively focused tree root now has active property', function () { @@ -1985,15 +2001,15 @@ suite('FocusManager', function () { .getRootFocusableNode() .getFocusableElement(); const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2009,15 +2025,15 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2030,10 +2046,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableNestedTree4 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2045,10 +2061,10 @@ suite('FocusManager', function () { const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2062,21 +2078,21 @@ suite('FocusManager', function () { const prevNodeElem = this.testFocusableTree2Node1.getFocusableElement(); const currNodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(prevNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(currNodeElem.classList), - 'blocklyActiveFocus', + assert.includesClass( + currNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(currNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + currNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); }); @@ -2091,7 +2107,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableGroup1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup1, ); @@ -2103,7 +2119,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableGroup1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup1, ); @@ -2116,7 +2132,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableGroup2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); @@ -2129,7 +2145,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableGroup2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); @@ -2142,7 +2158,7 @@ suite('FocusManager', function () { this.testFocusableGroup1.getRootFocusableNode(), ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup1, ); @@ -2153,7 +2169,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup1Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup1, ); @@ -2164,7 +2180,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup1Node1Child1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup1, ); @@ -2176,7 +2192,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup1Node2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup1, ); @@ -2189,7 +2205,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup2Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); @@ -2204,7 +2220,7 @@ suite('FocusManager', function () { this.testFocusableGroup2.getRootFocusableNode(), ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); @@ -2249,7 +2265,7 @@ suite('FocusManager', function () { this.focusManager.unregisterTree(this.testFocusableGroup1); // Since the most recent tree still exists, it still has focus. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); @@ -2261,7 +2277,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableNestedGroup4); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedGroup4, ); @@ -2273,7 +2289,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedGroup4, ); @@ -2286,7 +2302,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedGroup4, ); @@ -2298,7 +2314,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableGroup1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup1.getRootFocusableNode(), ); @@ -2312,7 +2328,7 @@ suite('FocusManager', function () { // The original node retains focus since the tree already holds focus (per focusTree's // contract). - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup1Node1, ); @@ -2325,7 +2341,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableGroup2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2.getRootFocusableNode(), ); @@ -2338,7 +2354,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableGroup2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2.getRootFocusableNode(), ); @@ -2351,7 +2367,7 @@ suite('FocusManager', function () { this.testFocusableGroup1.getRootFocusableNode(), ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup1.getRootFocusableNode(), ); @@ -2362,7 +2378,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup1Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup1Node1, ); @@ -2373,7 +2389,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup1Node1Child1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup1Node1Child1, ); @@ -2385,7 +2401,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup1Node2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup1Node2, ); @@ -2398,7 +2414,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup2Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); @@ -2413,7 +2429,7 @@ suite('FocusManager', function () { this.testFocusableGroup2.getRootFocusableNode(), ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2.getRootFocusableNode(), ); @@ -2458,7 +2474,7 @@ suite('FocusManager', function () { this.focusManager.unregisterTree(this.testFocusableGroup1); // Since the most recent tree still exists, it still has focus. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); @@ -2470,7 +2486,7 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableNestedGroup4); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedGroup4.getRootFocusableNode(), ); @@ -2482,7 +2498,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedGroup4Node1, ); @@ -2495,7 +2511,7 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedGroup4Node1, ); @@ -2510,10 +2526,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2526,10 +2542,10 @@ suite('FocusManager', function () { // The original node retains active focus since the tree already holds focus (per // focusTree's contract). const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2543,10 +2559,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup2 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2560,10 +2576,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup2 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2577,10 +2593,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2590,10 +2606,10 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup1Node1); const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2605,13 +2621,13 @@ suite('FocusManager', function () { const prevNodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2622,10 +2638,10 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup1Node2); const newNodeElem = this.testFocusableGroup1Node2.getFocusableElement(); - assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(newNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2638,13 +2654,13 @@ suite('FocusManager', function () { const prevNodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(prevNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2656,10 +2672,10 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup2Node1); const newNodeElem = this.testFocusableGroup2Node1.getFocusableElement(); - assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(newNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2675,10 +2691,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup2 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2692,10 +2708,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2707,10 +2723,10 @@ suite('FocusManager', function () { // Since the tree was unregistered it no longer has focus indicators. const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2728,21 +2744,21 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1.getFocusableElement(); const removedNodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.include( - Array.from(otherNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(otherNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2760,21 +2776,21 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1.getFocusableElement(); const removedNodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.include( - Array.from(otherNodeElem.classList), - 'blocklyActiveFocus', + assert.includesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(otherNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2790,8 +2806,8 @@ suite('FocusManager', function () { // passive now that the new node is active. const node1 = this.testFocusableGroup1Node1.getFocusableElement(); const node2 = this.testFocusableGroup1Node2.getFocusableElement(); - assert.notInclude(Array.from(node1.classList), 'blocklyPassiveFocus'); - assert.notInclude(Array.from(node2.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(node1.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass(node2.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('registered tree focusTree()ed other tree node passively focused tree node now has active property', function () { @@ -2808,15 +2824,15 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2832,15 +2848,15 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2853,10 +2869,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableNestedGroup4 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2868,10 +2884,10 @@ suite('FocusManager', function () { const nodeElem = this.testFocusableNestedGroup4Node1.getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -2886,21 +2902,21 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1.getFocusableElement(); const currNodeElem = this.testFocusableNestedGroup4Node1.getFocusableElement(); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(prevNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(currNodeElem.classList), - 'blocklyActiveFocus', + assert.includesClass( + currNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(currNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + currNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); }); @@ -2913,7 +2929,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup1, ); @@ -2924,7 +2940,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup1, ); @@ -2935,7 +2951,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node1.child1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup1, ); @@ -2947,7 +2963,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node2').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup1, ); @@ -2960,7 +2976,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup2.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); @@ -2973,7 +2989,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup2').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); @@ -2987,7 +3003,7 @@ suite('FocusManager', function () { .focus(); // The tree of the unregistered child element should take focus. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup1, ); @@ -3060,7 +3076,7 @@ suite('FocusManager', function () { this.focusManager.unregisterTree(this.testFocusableGroup1); // Since the most recent tree still exists, it still has focus. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); @@ -3087,7 +3103,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedGroup4').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedGroup4, ); @@ -3099,7 +3115,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedGroup4.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedGroup4, ); @@ -3112,7 +3128,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedGroup4.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedGroup4, ); @@ -3124,7 +3140,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup1.getRootFocusableNode(), ); @@ -3135,7 +3151,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup1Node1, ); @@ -3146,7 +3162,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node1.child1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup1Node1Child1, ); @@ -3158,7 +3174,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node2').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup1Node2, ); @@ -3171,7 +3187,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup2.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); @@ -3184,7 +3200,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup2').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2.getRootFocusableNode(), ); @@ -3198,7 +3214,7 @@ suite('FocusManager', function () { .focus(); // The nearest node of the unregistered child element should take focus. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup1Node2, ); @@ -3235,7 +3251,7 @@ suite('FocusManager', function () { document.getElementById('testUnfocusableElement').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup1Node1, ); @@ -3280,7 +3296,7 @@ suite('FocusManager', function () { this.focusManager.unregisterTree(this.testFocusableGroup1); // Since the most recent tree still exists, it still has focus. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); @@ -3307,7 +3323,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedGroup4').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedGroup4.getRootFocusableNode(), ); @@ -3319,7 +3335,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedGroup4.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedGroup4Node1, ); @@ -3332,7 +3348,7 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedGroup4.node1').focus(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedGroup4Node1, ); @@ -3347,10 +3363,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3360,10 +3376,10 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node1').focus(); const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3375,13 +3391,13 @@ suite('FocusManager', function () { const prevNodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3392,10 +3408,10 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node2').focus(); const newNodeElem = this.testFocusableGroup1Node2.getFocusableElement(); - assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(newNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3408,13 +3424,13 @@ suite('FocusManager', function () { const prevNodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(prevNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3426,10 +3442,10 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup2.node1').focus(); const newNodeElem = this.testFocusableGroup2Node1.getFocusableElement(); - assert.include(Array.from(newNodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(newNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3443,10 +3459,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup2 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3459,10 +3475,10 @@ suite('FocusManager', function () { // The nearest node of the unregistered child element should be actively focused. const nodeElem = this.testFocusableGroup1Node2.getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3474,10 +3490,10 @@ suite('FocusManager', function () { const rootElem = document.getElementById( 'testUnregisteredFocusableGroup3', ); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3491,10 +3507,10 @@ suite('FocusManager', function () { const nodeElem = document.getElementById( 'testUnregisteredFocusableGroup3.node1', ); - assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3510,18 +3526,18 @@ suite('FocusManager', function () { const attemptedNewNodeElem = document.getElementById( 'testUnfocusableElement', ); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(attemptedNewNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + attemptedNewNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(attemptedNewNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + attemptedNewNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3535,10 +3551,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3550,10 +3566,10 @@ suite('FocusManager', function () { // Since the tree was unregistered it no longer has focus indicators. const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3571,21 +3587,21 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1.getFocusableElement(); const removedNodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.include( - Array.from(otherNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(otherNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3603,21 +3619,21 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1.getFocusableElement(); const removedNodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.include( - Array.from(otherNodeElem.classList), - 'blocklyActiveFocus', + assert.includesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(otherNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3635,21 +3651,21 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1.getFocusableElement(); const removedNodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.notInclude( - Array.from(otherNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(otherNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(removedNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3665,8 +3681,8 @@ suite('FocusManager', function () { // passive now that the new node is active. const node1 = this.testFocusableGroup1Node1.getFocusableElement(); const node2 = this.testFocusableGroup1Node2.getFocusableElement(); - assert.notInclude(Array.from(node1.classList), 'blocklyPassiveFocus'); - assert.notInclude(Array.from(node2.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(node1.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass(node2.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('registered tree focus()ed other tree node passively focused tree root now has active property', function () { @@ -3683,15 +3699,15 @@ suite('FocusManager', function () { .getRootFocusableNode() .getFocusableElement(); const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3707,15 +3723,15 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3728,10 +3744,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableNestedGroup4 .getRootFocusableNode() .getFocusableElement(); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(rootElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3743,10 +3759,10 @@ suite('FocusManager', function () { const nodeElem = this.testFocusableNestedGroup4Node1.getFocusableElement(); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(nodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3761,21 +3777,21 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1.getFocusableElement(); const currNodeElem = this.testFocusableNestedGroup4Node1.getFocusableElement(); - assert.notInclude( - Array.from(prevNodeElem.classList), - 'blocklyActiveFocus', + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(prevNodeElem.classList), - 'blocklyPassiveFocus', + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.include( - Array.from(currNodeElem.classList), - 'blocklyActiveFocus', + assert.includesClass( + currNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude( - Array.from(currNodeElem.classList), - 'blocklyPassiveFocus', + assert.notIncludesClass( + currNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); }); @@ -3793,8 +3809,8 @@ suite('FocusManager', function () { const rootElem = rootNode.getFocusableElement(); assert.isNull(this.focusManager.getFocusedTree()); assert.isNull(this.focusManager.getFocusedNode()); - assert.include(Array.from(rootElem.classList), 'blocklyPassiveFocus'); - assert.notInclude(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.includesClass(rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('Defocusing actively focused HTML tree node switches to passive highlight', function () { @@ -3806,8 +3822,8 @@ suite('FocusManager', function () { const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); assert.isNull(this.focusManager.getFocusedTree()); assert.isNull(this.focusManager.getFocusedNode()); - assert.include(Array.from(nodeElem.classList), 'blocklyPassiveFocus'); - assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.includesClass(nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('Defocusing actively focused HTML subtree node switches to passive highlight', function () { @@ -3820,8 +3836,8 @@ suite('FocusManager', function () { const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); assert.isNull(this.focusManager.getFocusedTree()); assert.isNull(this.focusManager.getFocusedNode()); - assert.include(Array.from(nodeElem.classList), 'blocklyPassiveFocus'); - assert.notInclude(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.includesClass(nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('Refocusing actively focused root HTML tree restores to active highlight', function () { @@ -3833,10 +3849,10 @@ suite('FocusManager', function () { const rootNode = this.testFocusableTree2.getRootFocusableNode(); const rootElem = rootNode.getFocusableElement(); - assert.equal(this.focusManager.getFocusedTree(), this.testFocusableTree2); - assert.equal(this.focusManager.getFocusedNode(), rootNode); - assert.notInclude(Array.from(rootElem.classList), 'blocklyPassiveFocus'); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.strictEqual(this.focusManager.getFocusedTree(), this.testFocusableTree2); + assert.strictEqual(this.focusManager.getFocusedNode(), rootNode); + assert.notIncludesClass(rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('Refocusing actively focused HTML tree node restores to active highlight', function () { @@ -3847,13 +3863,13 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree2.node1').focus(); const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); - assert.equal(this.focusManager.getFocusedTree(), this.testFocusableTree2); - assert.equal( + assert.strictEqual(this.focusManager.getFocusedTree(), this.testFocusableTree2); + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); - assert.notInclude(Array.from(nodeElem.classList), 'blocklyPassiveFocus'); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notIncludesClass(nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('Refocusing actively focused HTML subtree node restores to active highlight', function () { @@ -3865,16 +3881,16 @@ suite('FocusManager', function () { document.getElementById('testFocusableNestedTree4.node1').focus(); const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableNestedTree4, ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableNestedTree4Node1, ); - assert.notInclude(Array.from(nodeElem.classList), 'blocklyPassiveFocus'); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.notIncludesClass(nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); }); }); @@ -3895,18 +3911,18 @@ suite('FocusManager', function () { const currElem = this.testFocusableGroup2 .getRootFocusableNode() .getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('HTML focusTree()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { @@ -3920,18 +3936,18 @@ suite('FocusManager', function () { .getRootFocusableNode() .getFocusableElement(); const currElem = this.testFocusableGroup2Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('HTML focusTree()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { @@ -3945,18 +3961,18 @@ suite('FocusManager', function () { .getRootFocusableNode() .getFocusableElement(); const currElem = this.testFocusableGroup2Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('HTML focusNode()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { @@ -3970,18 +3986,18 @@ suite('FocusManager', function () { const currElem = this.testFocusableGroup2 .getRootFocusableNode() .getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('HTML focusNode()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { @@ -3993,18 +4009,18 @@ suite('FocusManager', function () { const prevElem = this.testFocusableTree2Node1.getFocusableElement(); const currElem = this.testFocusableGroup2Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('HTML focusNode()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { @@ -4016,18 +4032,18 @@ suite('FocusManager', function () { const prevElem = this.testFocusableTree2Node1.getFocusableElement(); const currElem = this.testFocusableGroup2Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('HTML DOM focus()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { @@ -4041,18 +4057,18 @@ suite('FocusManager', function () { const currElem = this.testFocusableGroup2 .getRootFocusableNode() .getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('HTML DOM focus()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { @@ -4064,18 +4080,18 @@ suite('FocusManager', function () { const prevElem = this.testFocusableTree2Node1.getFocusableElement(); const currElem = this.testFocusableGroup2Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('HTML DOM focus()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { @@ -4087,18 +4103,18 @@ suite('FocusManager', function () { const prevElem = this.testFocusableTree2Node1.getFocusableElement(); const currElem = this.testFocusableGroup2Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); }); suite('Focus SVG tree then HTML tree', function () { @@ -4115,18 +4131,18 @@ suite('FocusManager', function () { const currElem = this.testFocusableTree2 .getRootFocusableNode() .getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree2, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('SVG focusTree()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { @@ -4140,18 +4156,18 @@ suite('FocusManager', function () { .getRootFocusableNode() .getFocusableElement(); const currElem = this.testFocusableTree2Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('SVG focusTree()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { @@ -4165,18 +4181,18 @@ suite('FocusManager', function () { .getRootFocusableNode() .getFocusableElement(); const currElem = this.testFocusableTree2Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('SVG focusNode()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { @@ -4190,18 +4206,18 @@ suite('FocusManager', function () { const currElem = this.testFocusableTree2 .getRootFocusableNode() .getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree2, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('SVG focusNode()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { @@ -4213,18 +4229,18 @@ suite('FocusManager', function () { const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); const currElem = this.testFocusableTree2Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('SVG focusNode()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { @@ -4236,18 +4252,18 @@ suite('FocusManager', function () { const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); const currElem = this.testFocusableTree2Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('SVG DOM focus()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { @@ -4261,18 +4277,18 @@ suite('FocusManager', function () { const currElem = this.testFocusableTree2 .getRootFocusableNode() .getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableTree2, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('SVG DOM focus()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { @@ -4284,18 +4300,18 @@ suite('FocusManager', function () { const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); const currElem = this.testFocusableTree2Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); test('SVG DOM focus()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { @@ -4307,18 +4323,18 @@ suite('FocusManager', function () { const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); const currElem = this.testFocusableTree2Node1.getFocusableElement(); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.include(Array.from(currElem.classList), 'blocklyActiveFocus'); - assert.notInclude( - Array.from(currElem.classList), - 'blocklyPassiveFocus', + assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notInclude(Array.from(prevElem.classList), 'blocklyActiveFocus'); - assert.include(Array.from(prevElem.classList), 'blocklyPassiveFocus'); + assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }); }); }); @@ -4326,10 +4342,6 @@ suite('FocusManager', function () { /* Ephemeral focus tests. */ suite('takeEphemeralFocus()', function () { - function classListOf(node) { - return Array.from(node.getFocusableElement().classList); - } - test('with no focused node does not change states', function () { this.focusManager.registerTree(this.testFocusableTree2); this.focusManager.registerTree(this.testFocusableGroup2); @@ -4341,10 +4353,12 @@ suite('FocusManager', function () { // Taking focus without an existing node having focus should change no focus indicators. const activeElems = Array.from( - document.querySelectorAll('.blocklyActiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); const passiveElems = Array.from( - document.querySelectorAll('.blocklyPassiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.PASSIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.isEmpty(activeElems); assert.isEmpty(passiveElems); @@ -4362,16 +4376,18 @@ suite('FocusManager', function () { // Taking focus without an existing node having focus should change no focus indicators. const activeElems = Array.from( - document.querySelectorAll('.blocklyActiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); const passiveElems = Array.from( - document.querySelectorAll('.blocklyPassiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.PASSIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.isEmpty(activeElems); - assert.equal(passiveElems.length, 1); - assert.include( - classListOf(this.testFocusableTree2Node1), - 'blocklyPassiveFocus', + assert.strictEqual(passiveElems.length, 1); + assert.includesClass( + this.testFocusableTree2Node1.getFocusableElement().classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -4429,12 +4445,13 @@ suite('FocusManager', function () { this.focusManager.focusTree(this.testFocusableGroup2); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); const activeElems = Array.from( - document.querySelectorAll('.blocklyActiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.isEmpty(activeElems); }); @@ -4450,12 +4467,13 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup2Node1); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); const activeElems = Array.from( - document.querySelectorAll('.blocklyActiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.isEmpty(activeElems); }); @@ -4474,12 +4492,13 @@ suite('FocusManager', function () { // The focus() state change will affect getFocusedNode() but it will not cause the node to now // be active. - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); const activeElems = Array.from( - document.querySelectorAll('.blocklyActiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.isEmpty(activeElems); }); @@ -4497,10 +4516,12 @@ suite('FocusManager', function () { // Finishing ephemeral focus without a previously focused node should not change indicators. const activeElems = Array.from( - document.querySelectorAll('.blocklyActiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); const passiveElems = Array.from( - document.querySelectorAll('.blocklyPassiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.PASSIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.isEmpty(activeElems); assert.isEmpty(passiveElems); @@ -4556,14 +4577,15 @@ suite('FocusManager', function () { // The original focused node should be restored. const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); const activeElems = Array.from( - document.querySelectorAll('.blocklyActiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); - assert.equal(activeElems.length, 1); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.strictEqual(activeElems.length, 1); + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); assert.strictEqual(document.activeElement, nodeElem); }); @@ -4586,14 +4608,15 @@ suite('FocusManager', function () { .getRootFocusableNode() .getFocusableElement(); const activeElems = Array.from( - document.querySelectorAll('.blocklyActiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedTree(), this.testFocusableGroup2, ); - assert.equal(activeElems.length, 1); - assert.include(Array.from(rootElem.classList), 'blocklyActiveFocus'); + assert.strictEqual(activeElems.length, 1); + assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); assert.strictEqual(document.activeElement, rootElem); }); @@ -4614,14 +4637,15 @@ suite('FocusManager', function () { // end of the ephemeral flow. const nodeElem = this.testFocusableGroup2Node1.getFocusableElement(); const activeElems = Array.from( - document.querySelectorAll('.blocklyActiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); - assert.equal(activeElems.length, 1); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.strictEqual(activeElems.length, 1); + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); assert.strictEqual(document.activeElement, nodeElem); }); @@ -4642,14 +4666,15 @@ suite('FocusManager', function () { // end of the ephemeral flow. const nodeElem = this.testFocusableGroup2Node1.getFocusableElement(); const activeElems = Array.from( - document.querySelectorAll('.blocklyActiveFocus'), + document.querySelectorAll( + FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); - assert.equal( + assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableGroup2Node1, ); - assert.equal(activeElems.length, 1); - assert.include(Array.from(nodeElem.classList), 'blocklyActiveFocus'); + assert.strictEqual(activeElems.length, 1); + assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); assert.strictEqual(document.activeElement, nodeElem); }); }); diff --git a/tests/mocha/focusable_tree_traverser_test.js b/tests/mocha/focusable_tree_traverser_test.js index 2069132fee7..00b2c539043 100644 --- a/tests/mocha/focusable_tree_traverser_test.js +++ b/tests/mocha/focusable_tree_traverser_test.js @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {FocusManager} from '../../build/src/core/focus_manager.js'; import {FocusableTreeTraverser} from '../../build/src/core/utils/focusable_tree_traverser.js'; import {assert} from '../../node_modules/chai/chai.js'; import { @@ -11,51 +12,59 @@ import { sharedTestTeardown, } from './test_helpers/setup_teardown.js'; +class FocusableNodeImpl { + constructor(element, tree) { + this.element = element; + this.tree = tree; + } + + getFocusableElement() { + return this.element; + } + + getFocusableTree() { + return this.tree; + } +} + +class FocusableTreeImpl { + constructor(rootElement, nestedTrees) { + this.nestedTrees = nestedTrees; + this.idToNodeMap = {}; + this.rootNode = this.addNode(rootElement); + } + + addNode(element) { + const node = new FocusableNodeImpl(element, this); + this.idToNodeMap[element.id] = node; + return node; + } + + getFocusedNode() { + throw Error('Unused in test suite.'); + } + + getRootFocusableNode() { + return this.rootNode; + } + + getNestedTrees() { + return this.nestedTrees; + } + + lookUpFocusableNode(id) { + return this.idToNodeMap[id]; + } + + findFocusableNodeFor(element) { + return FocusableTreeTraverser.findFocusableNodeFor(element, this); + } +} + suite('FocusableTreeTraverser', function () { setup(function () { sharedTestSetup.call(this); - const FocusableNodeImpl = function (element, tree) { - this.getFocusableElement = function () { - return element; - }; - - this.getFocusableTree = function () { - return tree; - }; - }; - const FocusableTreeImpl = function (rootElement, nestedTrees) { - this.idToNodeMap = {}; - - this.addNode = function (element) { - const node = new FocusableNodeImpl(element, this); - this.idToNodeMap[element.id] = node; - return node; - }; - - this.getFocusedNode = function () { - throw Error('Unused in test suite.'); - }; - - this.getRootFocusableNode = function () { - return this.rootNode; - }; - - this.getNestedTrees = function () { - return nestedTrees; - }; - - this.lookUpFocusableNode = function (id) { - return this.idToNodeMap[id]; - }; - - this.findFocusableNodeFor = function (element) { - return FocusableTreeTraverser.findFocusableNodeFor(element, this); - }; - - this.rootNode = this.addNode(rootElement); - }; - const createFocusableTree = function (rootElementId, nestedTrees) { return new FocusableTreeImpl( document.getElementById(rootElementId), @@ -107,7 +116,7 @@ suite('FocusableTreeTraverser', function () { sharedTestTeardown.call(this); const removeFocusIndicators = function (element) { - element.classList.remove('blocklyActiveFocus', 'blocklyPassiveFocus'); + element.classList.remove(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); }; // Ensure all node CSS styles are reset so that state isn't leaked between tests. @@ -141,101 +150,101 @@ suite('FocusableTreeTraverser', function () { test('for tree with root active highlight returns root node', function () { const tree = this.testFocusableTree1; const rootNode = tree.getRootFocusableNode(); - rootNode.getFocusableElement().classList.add('blocklyActiveFocus'); + rootNode.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); - assert.equal(finding, rootNode); + assert.strictEqual(finding, rootNode); }); test('for tree with root passive highlight returns root node', function () { const tree = this.testFocusableTree1; const rootNode = tree.getRootFocusableNode(); - rootNode.getFocusableElement().classList.add('blocklyPassiveFocus'); + rootNode.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); - assert.equal(finding, rootNode); + assert.strictEqual(finding, rootNode); }); test('for tree with node active highlight returns node', function () { const tree = this.testFocusableTree1; const node = this.testFocusableTree1Node1; - node.getFocusableElement().classList.add('blocklyActiveFocus'); + node.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); - assert.equal(finding, node); + assert.strictEqual(finding, node); }); test('for tree with node passive highlight returns node', function () { const tree = this.testFocusableTree1; const node = this.testFocusableTree1Node1; - node.getFocusableElement().classList.add('blocklyPassiveFocus'); + node.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); - assert.equal(finding, node); + assert.strictEqual(finding, node); }); test('for tree with nested node active highlight returns node', function () { const tree = this.testFocusableTree1; const node = this.testFocusableTree1Node1Child1; - node.getFocusableElement().classList.add('blocklyActiveFocus'); + node.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); - assert.equal(finding, node); + assert.strictEqual(finding, node); }); test('for tree with nested node passive highlight returns node', function () { const tree = this.testFocusableTree1; const node = this.testFocusableTree1Node1Child1; - node.getFocusableElement().classList.add('blocklyPassiveFocus'); + node.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); - assert.equal(finding, node); + assert.strictEqual(finding, node); }); - test('for tree with nested tree root active no parent highlights returns null', function () { + test('for tree with nested tree root active no parent highlights returns root', function () { const tree = this.testFocusableNestedTree4; const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); - rootNode.getFocusableElement().classList.add('blocklyActiveFocus'); + rootNode.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); - assert.equal(finding, rootNode); + assert.strictEqual(finding, rootNode); }); - test('for tree with nested tree root passive no parent highlights returns null', function () { + test('for tree with nested tree root passive no parent highlights returns root', function () { const tree = this.testFocusableNestedTree4; const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); - rootNode.getFocusableElement().classList.add('blocklyPassiveFocus'); + rootNode.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); - assert.equal(finding, rootNode); + assert.strictEqual(finding, rootNode); }); - test('for tree with nested tree root active no parent highlights returns null', function () { + test('for tree with nested tree node active no parent highlights returns node', function () { const tree = this.testFocusableNestedTree4; const node = this.testFocusableNestedTree4Node1; - node.getFocusableElement().classList.add('blocklyActiveFocus'); + node.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); - assert.equal(finding, node); + assert.strictEqual(finding, node); }); test('for tree with nested tree root passive no parent highlights returns null', function () { const tree = this.testFocusableNestedTree4; const node = this.testFocusableNestedTree4Node1; - node.getFocusableElement().classList.add('blocklyPassiveFocus'); + node.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); - assert.equal(finding, node); + assert.strictEqual(finding, node); }); test('for tree with nested tree root active parent node passive returns parent node', function () { @@ -243,14 +252,14 @@ suite('FocusableTreeTraverser', function () { const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); this.testFocusableTree2Node1 .getFocusableElement() - .classList.add('blocklyPassiveFocus'); - rootNode.getFocusableElement().classList.add('blocklyActiveFocus'); + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + rootNode.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode( this.testFocusableTree2, ); - assert.equal(finding, this.testFocusableTree2Node1); + assert.strictEqual(finding, this.testFocusableTree2Node1); }); test('for tree with nested tree root passive parent node passive returns parent node', function () { @@ -258,14 +267,14 @@ suite('FocusableTreeTraverser', function () { const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); this.testFocusableTree2Node1 .getFocusableElement() - .classList.add('blocklyPassiveFocus'); - rootNode.getFocusableElement().classList.add('blocklyPassiveFocus'); + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + rootNode.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode( this.testFocusableTree2, ); - assert.equal(finding, this.testFocusableTree2Node1); + assert.strictEqual(finding, this.testFocusableTree2Node1); }); test('for tree with nested tree node active parent node passive returns parent node', function () { @@ -273,14 +282,14 @@ suite('FocusableTreeTraverser', function () { const node = this.testFocusableNestedTree4Node1; this.testFocusableTree2Node1 .getFocusableElement() - .classList.add('blocklyPassiveFocus'); - node.getFocusableElement().classList.add('blocklyActiveFocus'); + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + node.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode( this.testFocusableTree2, ); - assert.equal(finding, this.testFocusableTree2Node1); + assert.strictEqual(finding, this.testFocusableTree2Node1); }); test('for tree with nested tree node passive parent node passive returns parent node', function () { @@ -288,14 +297,14 @@ suite('FocusableTreeTraverser', function () { const node = this.testFocusableNestedTree4Node1; this.testFocusableTree2Node1 .getFocusableElement() - .classList.add('blocklyPassiveFocus'); - node.getFocusableElement().classList.add('blocklyPassiveFocus'); + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + node.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode( this.testFocusableTree2, ); - assert.equal(finding, this.testFocusableTree2Node1); + assert.strictEqual(finding, this.testFocusableTree2Node1); }); }); @@ -310,7 +319,7 @@ suite('FocusableTreeTraverser', function () { tree, ); - assert.equal(finding, rootNode); + assert.strictEqual(finding, rootNode); }); test('for element for different tree root returns null', function () { @@ -347,7 +356,7 @@ suite('FocusableTreeTraverser', function () { tree, ); - assert.equal(finding, this.testFocusableTree1Node1); + assert.strictEqual(finding, this.testFocusableTree1Node1); }); test('for non-node element in tree returns root', function () { @@ -362,7 +371,7 @@ suite('FocusableTreeTraverser', function () { ); // An unregistered element should map to the closest node. - assert.equal(finding, this.testFocusableTree1Node2); + assert.strictEqual(finding, this.testFocusableTree1Node2); }); test('for nested node element in tree returns node', function () { @@ -375,7 +384,7 @@ suite('FocusableTreeTraverser', function () { ); // The nested node should be returned. - assert.equal(finding, this.testFocusableTree1Node1Child1); + assert.strictEqual(finding, this.testFocusableTree1Node1Child1); }); test('for nested node element in tree returns node', function () { @@ -390,7 +399,7 @@ suite('FocusableTreeTraverser', function () { ); // An unregistered element should map to the closest node. - assert.equal(finding, this.testFocusableTree1Node1Child1); + assert.strictEqual(finding, this.testFocusableTree1Node1Child1); }); test('for nested node element in tree returns node', function () { @@ -405,7 +414,7 @@ suite('FocusableTreeTraverser', function () { ); // An unregistered element should map to the closest node (or root). - assert.equal(finding, tree.getRootFocusableNode()); + assert.strictEqual(finding, tree.getRootFocusableNode()); }); test('for nested tree root returns nested tree root', function () { @@ -418,7 +427,7 @@ suite('FocusableTreeTraverser', function () { tree, ); - assert.equal(finding, rootNode); + assert.strictEqual(finding, rootNode); }); test('for nested tree node returns nested tree node', function () { @@ -431,7 +440,7 @@ suite('FocusableTreeTraverser', function () { ); // The node of the nested tree should be returned. - assert.equal(finding, this.testFocusableNestedTree4Node1); + assert.strictEqual(finding, this.testFocusableNestedTree4Node1); }); test('for nested element in nested tree node returns nearest nested node', function () { @@ -446,7 +455,7 @@ suite('FocusableTreeTraverser', function () { ); // An unregistered element should map to the closest node. - assert.equal(finding, this.testFocusableNestedTree4Node1); + assert.strictEqual(finding, this.testFocusableNestedTree4Node1); }); test('for nested tree node under root with different tree base returns null', function () { From 720e8dab2b2786887e2d0dbabb8a7e64d4a68d64 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 3 Apr 2025 22:55:35 +0000 Subject: [PATCH 119/222] chore: part 2 of addressing reviewer comments. --- core/focus_manager.ts | 35 ++++++-- core/interfaces/i_focusable_tree.ts | 32 ++----- core/utils/focusable_tree_traverser.ts | 39 ++++++--- tests/mocha/focus_manager_test.js | 89 ++++++-------------- tests/mocha/focusable_tree_traverser_test.js | 8 -- 5 files changed, 89 insertions(+), 114 deletions(-) diff --git a/core/focus_manager.ts b/core/focus_manager.ts index f0cc32b7402..228f659a824 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -6,6 +6,7 @@ import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; +import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.js'; import * as dom from './utils/dom.js'; /** @@ -30,7 +31,29 @@ export type ReturnEphemeralFocus = () => void; * focusNode(). */ export class FocusManager { + /** + * The CSS class assigned to IFocusableNode elements that presently have + * active DOM and Blockly focus. + * + * This should never be used directly. Instead, rely on FocusManager to ensure + * nodes have active focus (either automatically through DOM focus or manually + * through the various focus* methods provided by this class). + * + * It's recommended to not query using this class name, either. Instead, use + * FocusableTreeTraverser or IFocusableTree's methods to find a specific node. + */ static readonly ACTIVE_FOCUS_NODE_CSS_CLASS_NAME = 'blocklyActiveFocus'; + + /** + * The CSS class assigned to IFocusableNode elements that presently have + * passive focus (that is, they were the most recent node in their relative + * tree to have active focus--see ACTIVE_FOCUS_NODE_CSS_CLASS_NAME--and will + * receive active focus again if their surrounding tree is requested to become + * focused, i.e. using focusTree below). + * + * See ACTIVE_FOCUS_NODE_CSS_CLASS_NAME for caveats and limitations around + * using this constant directly (generally it never should need to be used). + */ static readonly PASSIVE_FOCUS_NODE_CSS_CLASS_NAME = 'blocklyPassiveFocus'; focusedNode: IFocusableNode | null = null; @@ -57,7 +80,8 @@ export class FocusManager { // updated. Per the contract of findFocusableNodeFor only one tree // should claim the element. for (const tree of this.registeredTrees) { - newNode = tree.findFocusableNodeFor(activeElement); + newNode = FocusableTreeTraverser.findFocusableNodeFor( + activeElement, tree); if (newNode) break; } } @@ -113,7 +137,7 @@ export class FocusManager { const treeIndex = this.registeredTrees.findIndex((tree) => tree === tree); this.registeredTrees.splice(treeIndex, 1); - const focusedNode = tree.getFocusedNode(); + const focusedNode = FocusableTreeTraverser.findFocusedNode(tree); const root = tree.getRootFocusableNode(); if (focusedNode) this.removeHighlight(focusedNode); if (this.focusedNode === focusedNode || this.focusedNode === root) { @@ -169,9 +193,8 @@ export class FocusManager { if (!this.isRegistered(focusableTree)) { throw Error(`Attempted to focus unregistered tree: ${focusableTree}.`); } - this.focusNode( - focusableTree.getFocusedNode() ?? focusableTree.getRootFocusableNode(), - ); + const currNode = FocusableTreeTraverser.findFocusedNode(focusableTree); + this.focusNode(currNode ?? focusableTree.getRootFocusableNode()); } /** @@ -193,7 +216,7 @@ export class FocusManager { this.setNodeToPassive(prevNode); } // If there's a focused node in the new node's tree, ensure it's reset. - const prevNodeNextTree = nextTree.getFocusedNode(); + const prevNodeNextTree = FocusableTreeTraverser.findFocusedNode(nextTree); const nextTreeRoot = nextTree.getRootFocusableNode(); if (prevNodeNextTree) { this.removeHighlight(prevNodeNextTree); diff --git a/core/interfaces/i_focusable_tree.ts b/core/interfaces/i_focusable_tree.ts index 9cedba732fa..bc0c38849c8 100644 --- a/core/interfaces/i_focusable_tree.ts +++ b/core/interfaces/i_focusable_tree.ts @@ -20,17 +20,14 @@ import type {IFocusableNode} from './i_focusable_node.js'; * page at any given time). The idea of passive focus is to provide context to * users on where their focus will be restored upon navigating back to a * previously focused tree. + * + * Note that if the tree's current focused node (passive or active) is needed, + * FocusableTreeTraverser.findFocusedNode can be used. + * + * Note that if specific nodes are needed to be retrieved for this tree, either + * use lookUpFocusableNode or FocusableTreeTraverser.findFocusableNodeFor. */ export interface IFocusableTree { - /** - * Returns the current node with focus in this tree, or null if none (or if - * the root has focus). - * - * Note that this will never return a node from a nested sub-tree as that tree - * should specifically be called in order to retrieve its focused node. - */ - getFocusedNode(): IFocusableNode | null; - /** * Returns the top-level focusable node of the tree. * @@ -61,21 +58,4 @@ export interface IFocusableTree { * @param id The ID of the node's focusable HTMLElement or SVGElement. */ lookUpFocusableNode(id: string): IFocusableNode | null; - - /** - * Returns the IFocusableNode corresponding to the select element, or null if - * the element does not have such a node. - * - * The provided element must have a non-null ID that conforms to the contract - * mentioned in IFocusableNode. - * - * This function may match against the root node of the tree. It will also map - * against the nearest node to the provided element if the element does not - * have an exact matching corresponding node. This function filters out - * matches against nested trees, so long as they are represented in the return - * value of getNestedTrees. - */ - findFocusableNodeFor( - element: HTMLElement | SVGElement, - ): IFocusableNode | null; } diff --git a/core/utils/focusable_tree_traverser.ts b/core/utils/focusable_tree_traverser.ts index 8061e981b50..6ea95b0b080 100644 --- a/core/utils/focusable_tree_traverser.ts +++ b/core/utils/focusable_tree_traverser.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {FocusManager} from '../focus_manager.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import * as dom from '../utils/dom.js'; @@ -14,24 +13,31 @@ import * as dom from '../utils/dom.js'; * tree traversals. */ export class FocusableTreeTraverser { - static readonly ACTIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME}`; - static readonly PASSIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME}`; + private static readonly ACTIVE_CLASS_NAME = 'blocklyActiveFocus'; + private static readonly PASSIVE_CSS_CLASS_NAME = 'blocklyPassiveFocus'; + private static readonly ACTIVE_FOCUS_NODE_CSS_SELECTOR = ( + `.${FocusableTreeTraverser.ACTIVE_CLASS_NAME}`); + private static readonly PASSIVE_FOCUS_NODE_CSS_SELECTOR = ( + `.${FocusableTreeTraverser.PASSIVE_CSS_CLASS_NAME}`); /** - * Returns the current IFocusableNode that either has the CSS class - * 'blocklyActiveFocus' or 'blocklyPassiveFocus', only considering HTML and - * SVG elements. + * Returns the current IFocusableNode that is styled (and thus represented) as + * having either passive or active focus, only considering HTML and SVG + * elements. * * This can match against the tree's root. * + * Note that this will never return a node from a nested sub-tree as that tree + * should specifically be used to retrieve its focused node. + * * @param tree The IFocusableTree in which to search for a focused node. * @returns The IFocusableNode currently with focus, or null if none. */ static findFocusedNode(tree: IFocusableTree): IFocusableNode | null { const root = tree.getRootFocusableNode().getFocusableElement(); if ( - dom.hasClass(root, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME) || - dom.hasClass(root, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME) + dom.hasClass(root, FocusableTreeTraverser.ACTIVE_CLASS_NAME) || + dom.hasClass(root, FocusableTreeTraverser.PASSIVE_CSS_CLASS_NAME) ) { // The root has focus. return tree.getRootFocusableNode(); @@ -39,7 +45,8 @@ export class FocusableTreeTraverser { const activeEl = root.querySelector(this.ACTIVE_FOCUS_NODE_CSS_SELECTOR); if (activeEl instanceof HTMLElement || activeEl instanceof SVGElement) { - const active = tree.findFocusableNodeFor(activeEl); + const active = FocusableTreeTraverser.findFocusableNodeFor( + activeEl, tree); if (active) return active; } @@ -47,7 +54,8 @@ export class FocusableTreeTraverser { // subtrees). const passiveEl = root.querySelector(this.PASSIVE_FOCUS_NODE_CSS_SELECTOR); if (passiveEl instanceof HTMLElement || passiveEl instanceof SVGElement) { - const passive = tree.findFocusableNodeFor(passiveEl); + const passive = FocusableTreeTraverser.findFocusableNodeFor( + passiveEl, tree); if (passive) return passive; } @@ -59,10 +67,17 @@ export class FocusableTreeTraverser { * element iff it's the root element or a descendent of the root element of * the specified IFocusableTree. * + * If the element exists within the specified tree's DOM structure but does + * not directly correspond to a node, the nearest parent node (or the tree's + * root) will be returned to represent the provided element. + * * If the tree contains another nested IFocusableTree, the nested tree may be * traversed but its nodes will never be returned here per the contract of * IFocusableTree.lookUpFocusableNode. * + * The provided element must have a non-null ID that conforms to the contract + * mentioned in IFocusableNode. + * * @param element The HTML or SVG element being sought. * @param tree The tree under which the provided element may be a descendant. * @returns The matching IFocusableNode, or null if there is no match. @@ -74,7 +89,9 @@ export class FocusableTreeTraverser { // First, match against subtrees. const subTreeMatches = tree .getNestedTrees() - .map((tree) => tree.findFocusableNodeFor(element)); + .map((tree) => { + return FocusableTreeTraverser.findFocusableNodeFor(element, tree); + }); if (subTreeMatches.findIndex((match) => !!match) !== -1) { // At least one subtree has a match for the element so it cannot be part // of the outer tree. diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js index fa8f12083f6..f61e9d37125 100644 --- a/tests/mocha/focus_manager_test.js +++ b/tests/mocha/focus_manager_test.js @@ -8,7 +8,6 @@ import { FocusManager, getFocusManager, } from '../../build/src/core/focus_manager.js'; -import {FocusableTreeTraverser} from '../../build/src/core/utils/focusable_tree_traverser.js'; import {assert} from '../../node_modules/chai/chai.js'; import { sharedTestSetup, @@ -43,10 +42,6 @@ class FocusableTreeImpl { return node; } - getFocusedNode() { - return FocusableTreeTraverser.findFocusedNode(this); - } - getRootFocusableNode() { return this.rootNode; } @@ -58,13 +53,14 @@ class FocusableTreeImpl { lookUpFocusableNode(id) { return this.idToNodeMap[id]; } - - findFocusableNodeFor(element) { - return FocusableTreeTraverser.findFocusableNodeFor(element, this); - } } suite('FocusManager', function () { + const ACTIVE_FOCUS_NODE_CSS_SELECTOR = ( + `.${FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME}`); + const PASSIVE_FOCUS_NODE_CSS_SELECTOR = ( + `.${FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME}`); + setup(function () { sharedTestSetup.call(this); @@ -160,35 +156,15 @@ suite('FocusManager', function () { const eventListener = this.globalDocumentEventListener; document.removeEventListener(eventType, eventListener); - const removeFocusIndicators = function (element) { - element.classList.remove(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - }; - // Ensure all node CSS styles are reset so that state isn't leaked between tests. - removeFocusIndicators(document.getElementById('testFocusableTree1')); - removeFocusIndicators(document.getElementById('testFocusableTree1.node1')); - removeFocusIndicators( - document.getElementById('testFocusableTree1.node1.child1'), - ); - removeFocusIndicators(document.getElementById('testFocusableTree1.node2')); - removeFocusIndicators(document.getElementById('testFocusableTree2')); - removeFocusIndicators(document.getElementById('testFocusableTree2.node1')); - removeFocusIndicators(document.getElementById('testFocusableNestedTree4')); - removeFocusIndicators( - document.getElementById('testFocusableNestedTree4.node1'), - ); - removeFocusIndicators(document.getElementById('testFocusableGroup1')); - removeFocusIndicators(document.getElementById('testFocusableGroup1.node1')); - removeFocusIndicators( - document.getElementById('testFocusableGroup1.node1.child1'), - ); - removeFocusIndicators(document.getElementById('testFocusableGroup1.node2')); - removeFocusIndicators(document.getElementById('testFocusableGroup2')); - removeFocusIndicators(document.getElementById('testFocusableGroup2.node1')); - removeFocusIndicators(document.getElementById('testFocusableNestedGroup4')); - removeFocusIndicators( - document.getElementById('testFocusableNestedGroup4.node1'), - ); + const activeElems = document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR); + const passiveElems = document.querySelectorAll(PASSIVE_FOCUS_NODE_CSS_SELECTOR); + for (const elem of activeElems) { + elem.classList.remove(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + } + for (const elem of passiveElems) { + elem.classList.remove(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + } // Reset the current active element. document.body.focus(); @@ -4353,12 +4329,10 @@ suite('FocusManager', function () { // Taking focus without an existing node having focus should change no focus indicators. const activeElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); const passiveElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.PASSIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(PASSIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.isEmpty(activeElems); assert.isEmpty(passiveElems); @@ -4376,12 +4350,10 @@ suite('FocusManager', function () { // Taking focus without an existing node having focus should change no focus indicators. const activeElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); const passiveElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.PASSIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(PASSIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.isEmpty(activeElems); assert.strictEqual(passiveElems.length, 1); @@ -4450,8 +4422,7 @@ suite('FocusManager', function () { this.testFocusableGroup2, ); const activeElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.isEmpty(activeElems); }); @@ -4472,8 +4443,7 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1, ); const activeElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.isEmpty(activeElems); }); @@ -4497,8 +4467,7 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1, ); const activeElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.isEmpty(activeElems); }); @@ -4516,12 +4485,10 @@ suite('FocusManager', function () { // Finishing ephemeral focus without a previously focused node should not change indicators. const activeElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); const passiveElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.PASSIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(PASSIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.isEmpty(activeElems); assert.isEmpty(passiveElems); @@ -4577,8 +4544,7 @@ suite('FocusManager', function () { // The original focused node should be restored. const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); const activeElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.strictEqual( this.focusManager.getFocusedNode(), @@ -4608,8 +4574,7 @@ suite('FocusManager', function () { .getRootFocusableNode() .getFocusableElement(); const activeElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.strictEqual( this.focusManager.getFocusedTree(), @@ -4637,8 +4602,7 @@ suite('FocusManager', function () { // end of the ephemeral flow. const nodeElem = this.testFocusableGroup2Node1.getFocusableElement(); const activeElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.strictEqual( this.focusManager.getFocusedNode(), @@ -4666,8 +4630,7 @@ suite('FocusManager', function () { // end of the ephemeral flow. const nodeElem = this.testFocusableGroup2Node1.getFocusableElement(); const activeElems = Array.from( - document.querySelectorAll( - FocusableTreeTraverser.ACTIVE_FOCUS_NODE_CSS_SELECTOR), + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), ); assert.strictEqual( this.focusManager.getFocusedNode(), diff --git a/tests/mocha/focusable_tree_traverser_test.js b/tests/mocha/focusable_tree_traverser_test.js index 00b2c539043..59eae38c172 100644 --- a/tests/mocha/focusable_tree_traverser_test.js +++ b/tests/mocha/focusable_tree_traverser_test.js @@ -40,10 +40,6 @@ class FocusableTreeImpl { return node; } - getFocusedNode() { - throw Error('Unused in test suite.'); - } - getRootFocusableNode() { return this.rootNode; } @@ -55,10 +51,6 @@ class FocusableTreeImpl { lookUpFocusableNode(id) { return this.idToNodeMap[id]; } - - findFocusableNodeFor(element) { - return FocusableTreeTraverser.findFocusableNodeFor(element, this); - } } suite('FocusableTreeTraverser', function () { From 58cd954fc00d10a036430a573b4b700c3c12ff8e Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 3 Apr 2025 15:59:34 -0700 Subject: [PATCH 120/222] feat: make getNextNode and getPreviousNode public (#8859) --- core/keyboard_nav/line_cursor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 8f3ac19b423..cf5317f0c83 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -299,7 +299,7 @@ export class LineCursor extends Marker { * should be traversed. * @returns The next node in the traversal. */ - private getNextNode( + getNextNode( node: ASTNode | null, isValid: (p1: ASTNode | null) => boolean, ): ASTNode | null { @@ -332,7 +332,7 @@ export class LineCursor extends Marker { * @returns The previous node in the traversal or null if no previous node * exists. */ - private getPreviousNode( + getPreviousNode( node: ASTNode | null, isValid: (p1: ASTNode | null) => boolean, ): ASTNode | null { From c5404af82e3542bb806a287a318c55dde4fd6bb1 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 3 Apr 2025 23:04:06 +0000 Subject: [PATCH 121/222] chore: lint fixes. --- core/blockly.ts | 8 +- core/focus_manager.ts | 6 +- core/utils/focusable_tree_traverser.ts | 22 +- tests/mocha/focus_manager_test.js | 770 +++++++++++++++---- tests/mocha/focusable_tree_traverser_test.js | 61 +- 5 files changed, 678 insertions(+), 189 deletions(-) diff --git a/core/blockly.ts b/core/blockly.ts index a98f0f695bb..1a06014f779 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -106,7 +106,11 @@ import {FlyoutItem} from './flyout_item.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import {FlyoutSeparator} from './flyout_separator.js'; import {VerticalFlyout} from './flyout_vertical.js'; -import {FocusManager, getFocusManager, ReturnEphemeralFocus} from './focus_manager.js'; +import { + FocusManager, + ReturnEphemeralFocus, + getFocusManager, +} from './focus_manager.js'; import {CodeGenerator} from './generator.js'; import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; @@ -523,7 +527,6 @@ export { FlyoutMetricsManager, FlyoutSeparator, FocusManager, - ReturnEphemeralFocus, CodeGenerator as Generator, Gesture, Grid, @@ -588,6 +591,7 @@ export { Names, Options, RenderedConnection, + ReturnEphemeralFocus, Scrollbar, ScrollbarPair, SeparatorFlyoutInflater, diff --git a/core/focus_manager.ts b/core/focus_manager.ts index 228f659a824..c1fc295b991 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -6,8 +6,8 @@ import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; -import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.js'; import * as dom from './utils/dom.js'; +import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.js'; /** * Type declaration for returning focus to FocusManager upon completing an @@ -81,7 +81,9 @@ export class FocusManager { // should claim the element. for (const tree of this.registeredTrees) { newNode = FocusableTreeTraverser.findFocusableNodeFor( - activeElement, tree); + activeElement, + tree, + ); if (newNode) break; } } diff --git a/core/utils/focusable_tree_traverser.ts b/core/utils/focusable_tree_traverser.ts index 6ea95b0b080..94603edd01b 100644 --- a/core/utils/focusable_tree_traverser.ts +++ b/core/utils/focusable_tree_traverser.ts @@ -15,10 +15,8 @@ import * as dom from '../utils/dom.js'; export class FocusableTreeTraverser { private static readonly ACTIVE_CLASS_NAME = 'blocklyActiveFocus'; private static readonly PASSIVE_CSS_CLASS_NAME = 'blocklyPassiveFocus'; - private static readonly ACTIVE_FOCUS_NODE_CSS_SELECTOR = ( - `.${FocusableTreeTraverser.ACTIVE_CLASS_NAME}`); - private static readonly PASSIVE_FOCUS_NODE_CSS_SELECTOR = ( - `.${FocusableTreeTraverser.PASSIVE_CSS_CLASS_NAME}`); + private static readonly ACTIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusableTreeTraverser.ACTIVE_CLASS_NAME}`; + private static readonly PASSIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusableTreeTraverser.PASSIVE_CSS_CLASS_NAME}`; /** * Returns the current IFocusableNode that is styled (and thus represented) as @@ -46,7 +44,9 @@ export class FocusableTreeTraverser { const activeEl = root.querySelector(this.ACTIVE_FOCUS_NODE_CSS_SELECTOR); if (activeEl instanceof HTMLElement || activeEl instanceof SVGElement) { const active = FocusableTreeTraverser.findFocusableNodeFor( - activeEl, tree); + activeEl, + tree, + ); if (active) return active; } @@ -55,7 +55,9 @@ export class FocusableTreeTraverser { const passiveEl = root.querySelector(this.PASSIVE_FOCUS_NODE_CSS_SELECTOR); if (passiveEl instanceof HTMLElement || passiveEl instanceof SVGElement) { const passive = FocusableTreeTraverser.findFocusableNodeFor( - passiveEl, tree); + passiveEl, + tree, + ); if (passive) return passive; } @@ -87,11 +89,9 @@ export class FocusableTreeTraverser { tree: IFocusableTree, ): IFocusableNode | null { // First, match against subtrees. - const subTreeMatches = tree - .getNestedTrees() - .map((tree) => { - return FocusableTreeTraverser.findFocusableNodeFor(element, tree); - }); + const subTreeMatches = tree.getNestedTrees().map((tree) => { + return FocusableTreeTraverser.findFocusableNodeFor(element, tree); + }); if (subTreeMatches.findIndex((match) => !!match) !== -1) { // At least one subtree has a match for the element so it cannot be part // of the outer tree. diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js index f61e9d37125..4a3f6b3ad1f 100644 --- a/tests/mocha/focus_manager_test.js +++ b/tests/mocha/focus_manager_test.js @@ -56,10 +56,8 @@ class FocusableTreeImpl { } suite('FocusManager', function () { - const ACTIVE_FOCUS_NODE_CSS_SELECTOR = ( - `.${FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME}`); - const PASSIVE_FOCUS_NODE_CSS_SELECTOR = ( - `.${FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME}`); + const ACTIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME}`; + const PASSIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME}`; setup(function () { sharedTestSetup.call(this); @@ -157,8 +155,12 @@ suite('FocusManager', function () { document.removeEventListener(eventType, eventListener); // Ensure all node CSS styles are reset so that state isn't leaked between tests. - const activeElems = document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR); - const passiveElems = document.querySelectorAll(PASSIVE_FOCUS_NODE_CSS_SELECTOR); + const activeElems = document.querySelectorAll( + ACTIVE_FOCUS_NODE_CSS_SELECTOR, + ); + const passiveElems = document.querySelectorAll( + PASSIVE_FOCUS_NODE_CSS_SELECTOR, + ); for (const elem of activeElems) { elem.classList.remove(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); } @@ -170,12 +172,18 @@ suite('FocusManager', function () { document.body.focus(); }); - assert.includesClass = function(classList, className) { - assert.isTrue(classList.contains(className), 'Expected class list to include: ' + className); + assert.includesClass = function (classList, className) { + assert.isTrue( + classList.contains(className), + 'Expected class list to include: ' + className, + ); }; - assert.notIncludesClass = function(classList, className) { - assert.isFalse(classList.contains(className), 'Expected class list to not include: ' + className); + assert.notIncludesClass = function (classList, className) { + assert.isFalse( + classList.contains(className), + 'Expected class list to not include: ' + className, + ); }; /* Basic lifecycle tests. */ @@ -810,7 +818,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -826,7 +837,10 @@ suite('FocusManager', function () { // The original node retains active focus since the tree already holds focus (per // focusTree's contract). const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -843,7 +857,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree2 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -860,7 +877,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree2 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -877,7 +897,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -890,7 +913,10 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree1Node1); const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -921,7 +947,10 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree1Node2); const newNodeElem = this.testFocusableTree1Node2.getFocusableElement(); - assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( newNodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -954,7 +983,10 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableTree2Node1); const newNodeElem = this.testFocusableTree2Node1.getFocusableElement(); - assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( newNodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -973,7 +1005,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree2 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -990,7 +1025,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1005,7 +1043,10 @@ suite('FocusManager', function () { // Since the tree was unregistered it no longer has focus indicators. const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1088,8 +1129,14 @@ suite('FocusManager', function () { // passive now that the new node is active. const node1 = this.testFocusableTree1Node1.getFocusableElement(); const node2 = this.testFocusableTree1Node2.getFocusableElement(); - assert.notIncludesClass(node1.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.notIncludesClass(node2.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + node1.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + node2.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('registered tree focusTree()ed other tree node passively focused tree node now has active property', function () { @@ -1106,12 +1153,18 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1130,12 +1183,18 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1151,7 +1210,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableNestedTree4 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1166,7 +1228,10 @@ suite('FocusManager', function () { const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1645,7 +1710,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1658,7 +1726,10 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node1').focus(); const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1689,7 +1760,10 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1.node2').focus(); const newNodeElem = this.testFocusableTree1Node2.getFocusableElement(); - assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( newNodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1722,7 +1796,10 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree2.node1').focus(); const newNodeElem = this.testFocusableTree2Node1.getFocusableElement(); - assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( newNodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1739,7 +1816,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree2 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1755,7 +1835,10 @@ suite('FocusManager', function () { // The nearest node of the unregistered child element should be actively focused. const nodeElem = this.testFocusableTree1Node2.getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1770,7 +1853,10 @@ suite('FocusManager', function () { const rootElem = document.getElementById( 'testUnregisteredFocusableTree3', ); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1785,7 +1871,10 @@ suite('FocusManager', function () { const nodeElem = document.getElementById( 'testUnregisteredFocusableTree3.node1', ); - assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1804,7 +1893,10 @@ suite('FocusManager', function () { const attemptedNewNodeElem = document.getElementById( 'testUnfocusableElement', ); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1829,7 +1921,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1844,7 +1939,10 @@ suite('FocusManager', function () { // Since the tree was unregistered it no longer has focus indicators. const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -1959,8 +2057,14 @@ suite('FocusManager', function () { // passive now that the new node is active. const node1 = this.testFocusableTree1Node1.getFocusableElement(); const node2 = this.testFocusableTree1Node2.getFocusableElement(); - assert.notIncludesClass(node1.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.notIncludesClass(node2.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + node1.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + node2.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('registered tree focus()ed other tree node passively focused tree root now has active property', function () { @@ -1977,12 +2081,18 @@ suite('FocusManager', function () { .getRootFocusableNode() .getFocusableElement(); const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2001,12 +2111,18 @@ suite('FocusManager', function () { const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2022,7 +2138,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableNestedTree4 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2037,7 +2156,10 @@ suite('FocusManager', function () { const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2502,7 +2624,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2518,7 +2643,10 @@ suite('FocusManager', function () { // The original node retains active focus since the tree already holds focus (per // focusTree's contract). const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2535,7 +2663,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup2 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2552,7 +2683,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup2 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2569,7 +2703,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2582,7 +2719,10 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup1Node1); const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2614,7 +2754,10 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup1Node2); const newNodeElem = this.testFocusableGroup1Node2.getFocusableElement(); - assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( newNodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2648,7 +2791,10 @@ suite('FocusManager', function () { this.focusManager.focusNode(this.testFocusableGroup2Node1); const newNodeElem = this.testFocusableGroup2Node1.getFocusableElement(); - assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( newNodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2667,7 +2813,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup2 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2684,7 +2833,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2699,7 +2851,10 @@ suite('FocusManager', function () { // Since the tree was unregistered it no longer has focus indicators. const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2782,8 +2937,14 @@ suite('FocusManager', function () { // passive now that the new node is active. const node1 = this.testFocusableGroup1Node1.getFocusableElement(); const node2 = this.testFocusableGroup1Node2.getFocusableElement(); - assert.notIncludesClass(node1.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.notIncludesClass(node2.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + node1.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + node2.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('registered tree focusTree()ed other tree node passively focused tree node now has active property', function () { @@ -2800,12 +2961,18 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2824,12 +2991,18 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2845,7 +3018,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableNestedGroup4 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -2860,7 +3036,10 @@ suite('FocusManager', function () { const nodeElem = this.testFocusableNestedGroup4Node1.getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3339,7 +3518,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3352,7 +3534,10 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node1').focus(); const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3384,7 +3569,10 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1.node2').focus(); const newNodeElem = this.testFocusableGroup1Node2.getFocusableElement(); - assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( newNodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3418,7 +3606,10 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup2.node1').focus(); const newNodeElem = this.testFocusableGroup2Node1.getFocusableElement(); - assert.includesClass(newNodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( newNodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3435,7 +3626,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup2 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3451,7 +3645,10 @@ suite('FocusManager', function () { // The nearest node of the unregistered child element should be actively focused. const nodeElem = this.testFocusableGroup1Node2.getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3466,7 +3663,10 @@ suite('FocusManager', function () { const rootElem = document.getElementById( 'testUnregisteredFocusableGroup3', ); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3483,10 +3683,13 @@ suite('FocusManager', function () { const nodeElem = document.getElementById( 'testUnregisteredFocusableGroup3.node1', ); - assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); assert.notIncludesClass( nodeElem.classList, - FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3502,7 +3705,10 @@ suite('FocusManager', function () { const attemptedNewNodeElem = document.getElementById( 'testUnfocusableElement', ); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3527,7 +3733,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3542,7 +3751,10 @@ suite('FocusManager', function () { // Since the tree was unregistered it no longer has focus indicators. const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3657,8 +3869,14 @@ suite('FocusManager', function () { // passive now that the new node is active. const node1 = this.testFocusableGroup1Node1.getFocusableElement(); const node2 = this.testFocusableGroup1Node2.getFocusableElement(); - assert.notIncludesClass(node1.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.notIncludesClass(node2.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + node1.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + node2.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('registered tree focus()ed other tree node passively focused tree root now has active property', function () { @@ -3675,12 +3893,18 @@ suite('FocusManager', function () { .getRootFocusableNode() .getFocusableElement(); const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3699,12 +3923,18 @@ suite('FocusManager', function () { const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3720,7 +3950,10 @@ suite('FocusManager', function () { const rootElem = this.testFocusableNestedGroup4 .getRootFocusableNode() .getFocusableElement(); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3735,7 +3968,10 @@ suite('FocusManager', function () { const nodeElem = this.testFocusableNestedGroup4Node1.getFocusableElement(); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, @@ -3785,8 +4021,14 @@ suite('FocusManager', function () { const rootElem = rootNode.getFocusableElement(); assert.isNull(this.focusManager.getFocusedTree()); assert.isNull(this.focusManager.getFocusedNode()); - assert.includesClass(rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.notIncludesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('Defocusing actively focused HTML tree node switches to passive highlight', function () { @@ -3798,8 +4040,14 @@ suite('FocusManager', function () { const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); assert.isNull(this.focusManager.getFocusedTree()); assert.isNull(this.focusManager.getFocusedNode()); - assert.includesClass(nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('Defocusing actively focused HTML subtree node switches to passive highlight', function () { @@ -3812,8 +4060,14 @@ suite('FocusManager', function () { const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); assert.isNull(this.focusManager.getFocusedTree()); assert.isNull(this.focusManager.getFocusedNode()); - assert.includesClass(nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.notIncludesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('Refocusing actively focused root HTML tree restores to active highlight', function () { @@ -3825,10 +4079,19 @@ suite('FocusManager', function () { const rootNode = this.testFocusableTree2.getRootFocusableNode(); const rootElem = rootNode.getFocusableElement(); - assert.strictEqual(this.focusManager.getFocusedTree(), this.testFocusableTree2); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); assert.strictEqual(this.focusManager.getFocusedNode(), rootNode); - assert.notIncludesClass(rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('Refocusing actively focused HTML tree node restores to active highlight', function () { @@ -3839,13 +4102,22 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree2.node1').focus(); const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); - assert.strictEqual(this.focusManager.getFocusedTree(), this.testFocusableTree2); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); assert.strictEqual( this.focusManager.getFocusedNode(), this.testFocusableTree2Node1, ); - assert.notIncludesClass(nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('Refocusing actively focused HTML subtree node restores to active highlight', function () { @@ -3865,8 +4137,14 @@ suite('FocusManager', function () { this.focusManager.getFocusedNode(), this.testFocusableNestedTree4Node1, ); - assert.notIncludesClass(nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); }); @@ -3892,13 +4170,22 @@ suite('FocusManager', function () { this.testFocusableGroup2, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('HTML focusTree()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { @@ -3917,13 +4204,22 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('HTML focusTree()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { @@ -3942,13 +4238,22 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('HTML focusNode()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { @@ -3967,13 +4272,22 @@ suite('FocusManager', function () { this.testFocusableGroup2, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('HTML focusNode()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { @@ -3990,13 +4304,22 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('HTML focusNode()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { @@ -4013,13 +4336,22 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('HTML DOM focus()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { @@ -4038,13 +4370,22 @@ suite('FocusManager', function () { this.testFocusableGroup2, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('HTML DOM focus()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { @@ -4061,13 +4402,22 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('HTML DOM focus()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { @@ -4084,13 +4434,22 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); }); suite('Focus SVG tree then HTML tree', function () { @@ -4112,13 +4471,22 @@ suite('FocusManager', function () { this.testFocusableTree2, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('SVG focusTree()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { @@ -4137,13 +4505,22 @@ suite('FocusManager', function () { this.testFocusableTree2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('SVG focusTree()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { @@ -4162,13 +4539,22 @@ suite('FocusManager', function () { this.testFocusableTree2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('SVG focusNode()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { @@ -4187,13 +4573,22 @@ suite('FocusManager', function () { this.testFocusableTree2, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('SVG focusNode()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { @@ -4210,13 +4605,22 @@ suite('FocusManager', function () { this.testFocusableTree2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('SVG focusNode()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { @@ -4233,13 +4637,22 @@ suite('FocusManager', function () { this.testFocusableTree2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('SVG DOM focus()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { @@ -4258,13 +4671,22 @@ suite('FocusManager', function () { this.testFocusableTree2, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('SVG DOM focus()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { @@ -4281,13 +4703,22 @@ suite('FocusManager', function () { this.testFocusableTree2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); test('SVG DOM focus()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { @@ -4304,13 +4735,22 @@ suite('FocusManager', function () { this.testFocusableTree2Node1, ); assert.strictEqual(document.activeElement, currElem); - assert.includesClass(currElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.notIncludesClass( currElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); - assert.notIncludesClass(prevElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); - assert.includesClass(prevElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }); }); }); @@ -4551,7 +4991,10 @@ suite('FocusManager', function () { this.testFocusableTree2Node1, ); assert.strictEqual(activeElems.length, 1); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.strictEqual(document.activeElement, nodeElem); }); @@ -4581,7 +5024,10 @@ suite('FocusManager', function () { this.testFocusableGroup2, ); assert.strictEqual(activeElems.length, 1); - assert.includesClass(rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.strictEqual(document.activeElement, rootElem); }); @@ -4609,7 +5055,10 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1, ); assert.strictEqual(activeElems.length, 1); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.strictEqual(document.activeElement, nodeElem); }); @@ -4637,7 +5086,10 @@ suite('FocusManager', function () { this.testFocusableGroup2Node1, ); assert.strictEqual(activeElems.length, 1); - assert.includesClass(nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); assert.strictEqual(document.activeElement, nodeElem); }); }); diff --git a/tests/mocha/focusable_tree_traverser_test.js b/tests/mocha/focusable_tree_traverser_test.js index 59eae38c172..b6674573ecd 100644 --- a/tests/mocha/focusable_tree_traverser_test.js +++ b/tests/mocha/focusable_tree_traverser_test.js @@ -108,7 +108,10 @@ suite('FocusableTreeTraverser', function () { sharedTestTeardown.call(this); const removeFocusIndicators = function (element) { - element.classList.remove(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + element.classList.remove( + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); }; // Ensure all node CSS styles are reset so that state isn't leaked between tests. @@ -142,7 +145,9 @@ suite('FocusableTreeTraverser', function () { test('for tree with root active highlight returns root node', function () { const tree = this.testFocusableTree1; const rootNode = tree.getRootFocusableNode(); - rootNode.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + rootNode + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); @@ -152,7 +157,9 @@ suite('FocusableTreeTraverser', function () { test('for tree with root passive highlight returns root node', function () { const tree = this.testFocusableTree1; const rootNode = tree.getRootFocusableNode(); - rootNode.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + rootNode + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); @@ -162,7 +169,9 @@ suite('FocusableTreeTraverser', function () { test('for tree with node active highlight returns node', function () { const tree = this.testFocusableTree1; const node = this.testFocusableTree1Node1; - node.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + node + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); @@ -172,7 +181,9 @@ suite('FocusableTreeTraverser', function () { test('for tree with node passive highlight returns node', function () { const tree = this.testFocusableTree1; const node = this.testFocusableTree1Node1; - node.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + node + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); @@ -182,7 +193,9 @@ suite('FocusableTreeTraverser', function () { test('for tree with nested node active highlight returns node', function () { const tree = this.testFocusableTree1; const node = this.testFocusableTree1Node1Child1; - node.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + node + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); @@ -192,7 +205,9 @@ suite('FocusableTreeTraverser', function () { test('for tree with nested node passive highlight returns node', function () { const tree = this.testFocusableTree1; const node = this.testFocusableTree1Node1Child1; - node.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + node + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); @@ -202,7 +217,9 @@ suite('FocusableTreeTraverser', function () { test('for tree with nested tree root active no parent highlights returns root', function () { const tree = this.testFocusableNestedTree4; const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); - rootNode.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + rootNode + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); @@ -212,7 +229,9 @@ suite('FocusableTreeTraverser', function () { test('for tree with nested tree root passive no parent highlights returns root', function () { const tree = this.testFocusableNestedTree4; const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); - rootNode.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + rootNode + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); @@ -222,7 +241,9 @@ suite('FocusableTreeTraverser', function () { test('for tree with nested tree node active no parent highlights returns node', function () { const tree = this.testFocusableNestedTree4; const node = this.testFocusableNestedTree4Node1; - node.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + node + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); @@ -232,7 +253,9 @@ suite('FocusableTreeTraverser', function () { test('for tree with nested tree root passive no parent highlights returns null', function () { const tree = this.testFocusableNestedTree4; const node = this.testFocusableNestedTree4Node1; - node.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + node + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode(tree); @@ -245,7 +268,9 @@ suite('FocusableTreeTraverser', function () { this.testFocusableTree2Node1 .getFocusableElement() .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - rootNode.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + rootNode + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode( this.testFocusableTree2, @@ -260,7 +285,9 @@ suite('FocusableTreeTraverser', function () { this.testFocusableTree2Node1 .getFocusableElement() .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - rootNode.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + rootNode + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode( this.testFocusableTree2, @@ -275,7 +302,9 @@ suite('FocusableTreeTraverser', function () { this.testFocusableTree2Node1 .getFocusableElement() .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - node.getFocusableElement().classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + node + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode( this.testFocusableTree2, @@ -290,7 +319,9 @@ suite('FocusableTreeTraverser', function () { this.testFocusableTree2Node1 .getFocusableElement() .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - node.getFocusableElement().classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + node + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); const finding = FocusableTreeTraverser.findFocusedNode( this.testFocusableTree2, From 49387ec78890091cde2cf77c1a98b7abaf3704e9 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Mon, 7 Apr 2025 12:44:40 -0700 Subject: [PATCH 122/222] feat: add shouldHealStack method (#8872) * feat: add shouldHealStack method * chore: format --- core/dragging/block_drag_strategy.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/core/dragging/block_drag_strategy.ts b/core/dragging/block_drag_strategy.ts index 9a2cd747cd4..d1e5cf8d33a 100644 --- a/core/dragging/block_drag_strategy.ts +++ b/core/dragging/block_drag_strategy.ts @@ -113,7 +113,7 @@ export class BlockDragStrategy implements IDragStrategy { this.workspace.setResizesEnabled(false); blockAnimation.disconnectUiStop(); - const healStack = !!e && (e.altKey || e.ctrlKey || e.metaKey); + const healStack = this.shouldHealStack(e); if (this.shouldDisconnect(healStack)) { this.disconnectBlock(healStack); @@ -122,6 +122,17 @@ export class BlockDragStrategy implements IDragStrategy { this.workspace.getLayerManager()?.moveToDragLayer(this.block); } + /** + * Get whether the drag should act on a single block or a block stack. + * + * @param e The instigating pointer event, if any. + * @returns True if just the initial block should be dragged out, false + * if all following blocks should also be dragged. + */ + protected shouldHealStack(e: PointerEvent | undefined) { + return !!e && (e.altKey || e.ctrlKey || e.metaKey); + } + /** Starts a drag on a shadow, recording the drag offset. */ private startDraggingShadow(e?: PointerEvent) { const parent = this.block.getParent(); From 76b02de65494c7850d66809ffbc8fd64001d6319 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Mon, 7 Apr 2025 13:52:15 -0700 Subject: [PATCH 123/222] feat: add getSearchRadius to BlockDragStrategy (#8871) --- core/dragging/block_drag_strategy.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/dragging/block_drag_strategy.ts b/core/dragging/block_drag_strategy.ts index d1e5cf8d33a..b53c131653b 100644 --- a/core/dragging/block_drag_strategy.ts +++ b/core/dragging/block_drag_strategy.ts @@ -326,9 +326,7 @@ export class BlockDragStrategy implements IDragStrategy { delta: Coordinate, ): ConnectionCandidate | null { const localConns = this.getLocalConnections(draggingBlock); - let radius = this.connectionCandidate - ? config.connectingSnapRadius - : config.snapRadius; + let radius = this.getSearchRadius(); let candidate = null; for (const conn of localConns) { @@ -346,6 +344,15 @@ export class BlockDragStrategy implements IDragStrategy { return candidate; } + /** + * Get the radius to use when searching for a nearby valid connection. + */ + protected getSearchRadius() { + return this.connectionCandidate + ? config.connectingSnapRadius + : config.snapRadius; + } + /** * Returns all of the connections we might connect to blocks on the workspace. * From 89194b2ead9e639867236b17396d1eb3ecd971ed Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Mon, 7 Apr 2025 17:29:00 -0700 Subject: [PATCH 124/222] fix: check potential variables for flyout variable fields (#8873) * fix: check potential variables for flyout variable fields * fix: format * chore: move comment --- core/field_variable.ts | 52 +++++++++++++++------------- core/workspace.ts | 1 - tests/mocha/field_variable_test.js | 55 +++++++++++++++++++++++------- 3 files changed, 71 insertions(+), 37 deletions(-) diff --git a/core/field_variable.ts b/core/field_variable.ts index ad9037e9671..2af3c4d057a 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -71,7 +71,8 @@ export class FieldVariable extends FieldDropdown { * field's value. Takes in a variable ID & returns a validated variable * ID, or null to abort the change. * @param variableTypes A list of the types of variables to include in the - * dropdown. Will only be used if config is not provided. + * dropdown. Pass `null` to include all types that exist on the + * workspace. Will only be used if config is not provided. * @param defaultType The type of variable to create if this field's value * is not explicitly set. Defaults to ''. Will only be used if config * is not provided. @@ -83,7 +84,7 @@ export class FieldVariable extends FieldDropdown { constructor( varName: string | null | typeof Field.SKIP_SETUP, validator?: FieldVariableValidator, - variableTypes?: string[], + variableTypes?: string[] | null, defaultType?: string, config?: FieldVariableConfig, ) { @@ -423,25 +424,27 @@ export class FieldVariable extends FieldDropdown { * Return a list of variable types to include in the dropdown. * * @returns Array of variable types. - * @throws {Error} if variableTypes is an empty array. */ private getVariableTypes(): string[] { - let variableTypes = this.variableTypes; - if (variableTypes === null) { - // If variableTypes is null, return all variable types. - if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) { - return this.sourceBlock_.workspace.getVariableMap().getTypes(); - } + if (this.variableTypes) return this.variableTypes; + + if (!this.sourceBlock_ || this.sourceBlock_.isDeadOrDying()) { + // We should include all types in the block's workspace, + // but the block is dead so just give up. + return ['']; } - variableTypes = variableTypes || ['']; - if (variableTypes.length === 0) { - // Throw an error if variableTypes is an empty list. - const name = this.getText(); - throw Error( - "'variableTypes' of field variable " + name + ' was an empty list', - ); + + // If variableTypes is null, return all variable types in the workspace. + let allTypes = this.sourceBlock_.workspace.getVariableMap().getTypes(); + if (this.sourceBlock_.isInFlyout) { + // If this block is in a flyout, we also need to check the potential variables + const potentialMap = + this.sourceBlock_.workspace.getPotentialVariableMap(); + if (!potentialMap) return allTypes; + allTypes = Array.from(new Set([...allTypes, ...potentialMap.getTypes()])); } - return variableTypes; + + return allTypes; } /** @@ -455,11 +458,15 @@ export class FieldVariable extends FieldDropdown { * value is not explicitly set. Defaults to ''. */ private setTypes(variableTypes: string[] | null = null, defaultType = '') { - // If you expected that the default type would be the same as the only entry - // in the variable types array, tell the Blockly team by commenting on - // #1499. - // Set the allowable variable types. Null means all types on the workspace. + const name = this.getText(); if (Array.isArray(variableTypes)) { + if (variableTypes.length === 0) { + // Throw an error if variableTypes is an empty list. + throw Error( + `'variableTypes' of field variable ${name} was an empty list. If you want to include all variable types, pass 'null' instead.`, + ); + } + // Make sure the default type is valid. let isInArray = false; for (let i = 0; i < variableTypes.length; i++) { @@ -477,8 +484,7 @@ export class FieldVariable extends FieldDropdown { } } else if (variableTypes !== null) { throw Error( - "'variableTypes' was not an array in the definition of " + - 'a FieldVariable', + `'variableTypes' was not an array or null in the definition of FieldVariable ${name}`, ); } // Only update the field once all checks pass. diff --git a/core/workspace.ts b/core/workspace.ts index 30238b91e7f..261da0f2475 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -854,7 +854,6 @@ export class Workspace implements IASTNodeLocation { * These exist in the flyout but not in the workspace. * * @returns The potential variable map. - * @internal */ getPotentialVariableMap(): IVariableMap< IVariableModel diff --git a/tests/mocha/field_variable_test.js b/tests/mocha/field_variable_test.js index 221305d7172..2dc8d35a55c 100644 --- a/tests/mocha/field_variable_test.js +++ b/tests/mocha/field_variable_test.js @@ -309,6 +309,24 @@ suite('Variable Fields', function () { assert.deepEqual(field.variableTypes, ['Type1']); assert.equal(field.defaultType, 'Type1'); }); + test('Empty list of types', function () { + assert.throws(function () { + const fieldVariable = new Blockly.FieldVariable( + 'name1', + undefined, + [], + ); + }); + }); + test('invalid value for list of types', function () { + assert.throws(function () { + const fieldVariable = new Blockly.FieldVariable( + 'name1', + undefined, + 'not an array', + ); + }); + }); test('JSON Definition', function () { const field = Blockly.FieldVariable.fromJson({ variable: 'test', @@ -353,13 +371,6 @@ suite('Variable Fields', function () { this.workspace.createVariable('name1', 'type1'); this.workspace.createVariable('name2', 'type2'); }); - test('variableTypes is undefined', function () { - // Expect that since variableTypes is undefined, only type empty string - // will be returned (regardless of what types are available on the workspace). - const fieldVariable = new Blockly.FieldVariable('name1'); - const resultTypes = fieldVariable.getVariableTypes(); - assert.deepEqual(resultTypes, ['']); - }); test('variableTypes is explicit', function () { // Expect that since variableTypes is defined, it will be the return // value, regardless of what types are available on the workspace. @@ -377,6 +388,17 @@ suite('Variable Fields', function () { 'Default type was wrong', ); }); + test('variableTypes is undefined', function () { + // Expect all variable types in the workspace to be returned, same + // as if variableTypes is null. + const fieldVariable = new Blockly.FieldVariable('name1'); + const mockBlock = createTestBlock(); + mockBlock.workspace = this.workspace; + fieldVariable.setSourceBlock(mockBlock); + + const resultTypes = fieldVariable.getVariableTypes(); + assert.deepEqual(resultTypes, ['type1', 'type2']); + }); test('variableTypes is null', function () { // Expect all variable types to be returned. // The field does not need to be initialized to do this--it just needs @@ -390,16 +412,23 @@ suite('Variable Fields', function () { const resultTypes = fieldVariable.getVariableTypes(); assert.deepEqual(resultTypes, ['type1', 'type2']); }); - test('variableTypes is the empty list', function () { - const fieldVariable = new Blockly.FieldVariable('name1'); + test('variableTypes is null and variable is in the flyout', function () { + // Expect all variable types in the workspace and + // flyout workspace to be returned. + const fieldVariable = new Blockly.FieldVariable('name1', undefined, null); const mockBlock = createTestBlock(); mockBlock.workspace = this.workspace; + + // Pretend this is a flyout workspace with potential variables + mockBlock.isInFlyout = true; + mockBlock.workspace.createPotentialVariableMap(); + mockBlock.workspace + .getPotentialVariableMap() + .createVariable('name3', 'type3'); fieldVariable.setSourceBlock(mockBlock); - fieldVariable.variableTypes = []; - assert.throws(function () { - fieldVariable.getVariableTypes(); - }); + const resultTypes = fieldVariable.getVariableTypes(); + assert.deepEqual(resultTypes, ['type1', 'type2', 'type3']); }); }); suite('Default types', function () { From a68827a5408d98d2a0f1c0ed435912f5408c5c72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 08:25:32 -0700 Subject: [PATCH 125/222] chore(deps): bump eslint from 9.17.0 to 9.24.0 (#8867) Bumps [eslint](https://github.com/eslint/eslint) from 9.17.0 to 9.24.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.17.0...v9.24.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 9.24.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 126 +++++++++++++++++++++++++++++++--------------- 1 file changed, 86 insertions(+), 40 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3123e79c71..d4a46c28a0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -274,12 +274,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", - "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -287,20 +288,35 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", - "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -324,6 +340,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -332,35 +349,52 @@ } }, "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", + "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", - "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "dev": true, + "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.13.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@gulp-sourcemaps/identity-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", @@ -528,10 +562,11 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -1839,6 +1874,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2659,6 +2695,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3807,21 +3844,23 @@ } }, "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", + "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.24.0", + "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -3829,7 +3868,7 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", + "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", @@ -3970,10 +4009,11 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -4080,6 +4120,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -4259,7 +4300,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -5591,10 +5633,11 @@ "dev": true }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -6086,7 +6129,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify": { "version": "1.0.2", @@ -7028,6 +7072,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -7791,6 +7836,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } From 2c05119ef28c9071f82f0c2c5cb225c39318ab6b Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Tue, 8 Apr 2025 12:06:05 -0700 Subject: [PATCH 126/222] fix: change css class for disabled block pattern (#8864) --- core/css.ts | 2 +- core/renderers/common/path_object.ts | 1 + core/renderers/zelos/constants.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/css.ts b/core/css.ts index eec45df1af9..0502edbf13a 100644 --- a/core/css.ts +++ b/core/css.ts @@ -198,7 +198,7 @@ let content = ` display: none; } -.blocklyDisabled>.blocklyPath { +.blocklyDisabledPattern>.blocklyPath { fill: var(--blocklyDisabledPattern); fill-opacity: .5; stroke-opacity: .5; diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index a1bfbccbd09..077f80bb741 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -200,6 +200,7 @@ export class PathObject implements IPathObject { */ protected updateDisabled_(disabled: boolean) { this.setClass_('blocklyDisabled', disabled); + this.setClass_('blocklyDisabledPattern', disabled); } /** diff --git a/core/renderers/zelos/constants.ts b/core/renderers/zelos/constants.ts index 73d3285df32..8cd36e02589 100644 --- a/core/renderers/zelos/constants.ts +++ b/core/renderers/zelos/constants.ts @@ -890,7 +890,7 @@ export class ConstantProvider extends BaseConstantProvider { `}`, // Disabled outline paths. - `${selector} .blocklyDisabled > .blocklyOutlinePath {`, + `${selector} .blocklyDisabledPattern > .blocklyOutlinePath {`, `fill: var(--blocklyDisabledPattern)`, `}`, From 7aff8669448bcf2070b6863aad606aca20594550 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Tue, 8 Apr 2025 12:17:58 -0700 Subject: [PATCH 127/222] release: update version to 12.0.0-beta.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 66b6b3794b4..1e422e98683 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockly", - "version": "12.0.0-beta.2", + "version": "12.0.0-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "12.0.0-beta.2", + "version": "12.0.0-beta.3", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 62fbeb2e08f..af6436abd0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "12.0.0-beta.2", + "version": "12.0.0-beta.3", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly" From c5736bba65c70495ba1e9bac4648a3662914fda4 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Thu, 10 Apr 2025 10:34:35 -0700 Subject: [PATCH 128/222] feat: make block and workspace implement IContextMenu (#8876) --- core/block_svg.ts | 4 +++- core/workspace_svg.ts | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 1b76ed3f1c4..ca753fc0a4e 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -16,7 +16,6 @@ import './events/events_selected.js'; import {Block} from './block.js'; import * as blockAnimations from './block_animations.js'; -import {IDeletable} from './blockly.js'; import * as browserEvents from './browser_events.js'; import {BlockCopyData, BlockPaster} from './clipboard/block_paster.js'; import * as common from './common.js'; @@ -41,7 +40,9 @@ import {WarningIcon} from './icons/warning_icon.js'; import type {Input} from './inputs/input.js'; import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {ICopyable} from './interfaces/i_copyable.js'; +import {IDeletable} from './interfaces/i_deletable.js'; import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js'; import {IIcon} from './interfaces/i_icon.js'; import * as internalConstants from './internal_constants.js'; @@ -72,6 +73,7 @@ export class BlockSvg implements IASTNodeLocationSvg, IBoundedElement, + IContextMenu, ICopyable, IDraggable, IDeletable diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 68a1bd939fd..7dcd5b5700f 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -41,6 +41,7 @@ import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {IDragTarget} from './interfaces/i_drag_target.js'; import type {IFlyout} from './interfaces/i_flyout.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; @@ -90,7 +91,10 @@ const ZOOM_TO_FIT_MARGIN = 20; * Class for a workspace. This is an onscreen area with optional trashcan, * scrollbars, bubbles, and dragging. */ -export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { +export class WorkspaceSvg + extends Workspace + implements IASTNodeLocationSvg, IContextMenu +{ /** * A wrapper function called when a resize event occurs. * You can pass the result to `eventHandling.unbind`. From 3160e3d321d6c54ce04fc2046e211a2a97ad2ac2 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 11 Apr 2025 10:13:10 -0700 Subject: [PATCH 129/222] feat: add getFirstNode and getLastNode to cursor with tests (#8878) * feat: add getFirstNode and getlastNode to line_cursor.ts * chore: add simple tests for getFirstNode and getLastNode * chore: broken tests for debugging * chore: additional cursor tests * chore: lint, format, reenable tasks --- core/keyboard_nav/line_cursor.ts | 37 +++ tests/mocha/cursor_test.js | 479 +++++++++++++++++++++++-------- 2 files changed, 403 insertions(+), 113 deletions(-) diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index cf5317f0c83..d8e0a472bad 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -756,6 +756,43 @@ export class LineCursor extends Marker { } } } + + /** + * Get the first navigable node on the workspace, or null if none exist. + * + * @returns The first navigable node on the workspace, or null. + */ + getFirstNode(): ASTNode | null { + const topBlocks = this.workspace.getTopBlocks(true); + if (!topBlocks.length) return null; + return ASTNode.createTopNode(topBlocks[0]); + } + + /** + * Get the last navigable node on the workspace, or null if none exist. + * + * @returns The last navigable node on the workspace, or null. + */ + getLastNode(): ASTNode | null { + // Loop back to last block if it exists. + const topBlocks = this.workspace.getTopBlocks(true); + if (!topBlocks.length) return null; + + // Find the last stack. + const lastTopBlockNode = ASTNode.createStackNode( + topBlocks[topBlocks.length - 1], + ); + let prevNode = lastTopBlockNode; + let nextNode: ASTNode | null = lastTopBlockNode; + // Iterate until you fall off the end of the stack. + while (nextNode) { + prevNode = nextNode; + nextNode = this.getNextNode(prevNode, (node) => { + return !!node; + }); + } + return prevNode; + } } registry.register(registry.Type.CURSOR, registry.DEFAULT, LineCursor); diff --git a/tests/mocha/cursor_test.js b/tests/mocha/cursor_test.js index 3242edd2a37..b2a38268866 100644 --- a/tests/mocha/cursor_test.js +++ b/tests/mocha/cursor_test.js @@ -12,124 +12,377 @@ import { } from './test_helpers/setup_teardown.js'; suite('Cursor', function () { - setup(function () { - sharedTestSetup.call(this); - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'input_statement', - 'message0': '%1 %2 %3 %4', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME1', - 'text': 'default', - }, - { - 'type': 'field_input', - 'name': 'NAME2', - 'text': 'default', - }, - { - 'type': 'input_value', - 'name': 'NAME3', - }, - { - 'type': 'input_statement', - 'name': 'NAME4', - }, - ], - 'previousStatement': null, - 'nextStatement': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'field_input', - 'message0': '%1', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - ], - 'output': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - ]); - this.workspace = Blockly.inject('blocklyDiv', {}); - this.cursor = this.workspace.getCursor(); - const blockA = this.workspace.newBlock('input_statement'); - const blockB = this.workspace.newBlock('input_statement'); - const blockC = this.workspace.newBlock('input_statement'); - const blockD = this.workspace.newBlock('input_statement'); - const blockE = this.workspace.newBlock('field_input'); + suite('Movement', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'input_statement', + 'message0': '%1 %2 %3 %4', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME1', + 'text': 'default', + }, + { + 'type': 'field_input', + 'name': 'NAME2', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'NAME3', + }, + { + 'type': 'input_statement', + 'name': 'NAME4', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'field_input', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + ], + 'output': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + ]); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.cursor = this.workspace.getCursor(); + const blockA = this.workspace.newBlock('input_statement'); + const blockB = this.workspace.newBlock('input_statement'); + const blockC = this.workspace.newBlock('input_statement'); + const blockD = this.workspace.newBlock('input_statement'); + const blockE = this.workspace.newBlock('field_input'); - blockA.nextConnection.connect(blockB.previousConnection); - blockA.inputList[0].connection.connect(blockE.outputConnection); - blockB.inputList[1].connection.connect(blockC.previousConnection); - this.cursor.drawer = null; - this.blocks = { - A: blockA, - B: blockB, - C: blockC, - D: blockD, - E: blockE, - }; - }); - teardown(function () { - sharedTestTeardown.call(this); - }); + blockA.nextConnection.connect(blockB.previousConnection); + blockA.inputList[0].connection.connect(blockE.outputConnection); + blockB.inputList[1].connection.connect(blockC.previousConnection); + this.cursor.drawer = null; + this.blocks = { + A: blockA, + B: blockB, + C: blockC, + D: blockD, + E: blockE, + }; + }); + teardown(function () { + sharedTestTeardown.call(this); + }); - test('Next - From a Previous connection go to the next block', function () { - const prevNode = ASTNode.createConnectionNode( - this.blocks.A.previousConnection, - ); - this.cursor.setCurNode(prevNode); - this.cursor.next(); - const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.A); - }); - test('Next - From a block go to its statement input', function () { - const prevNode = ASTNode.createBlockNode(this.blocks.B); - this.cursor.setCurNode(prevNode); - this.cursor.next(); - const curNode = this.cursor.getCurNode(); - assert.equal( - curNode.getLocation(), - this.blocks.B.getInput('NAME4').connection, - ); - }); + test('Next - From a Previous connection go to the next block', function () { + const prevNode = ASTNode.createConnectionNode( + this.blocks.A.previousConnection, + ); + this.cursor.setCurNode(prevNode); + this.cursor.next(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode.getLocation(), this.blocks.A); + }); + test('Next - From a block go to its statement input', function () { + const prevNode = ASTNode.createBlockNode(this.blocks.B); + this.cursor.setCurNode(prevNode); + this.cursor.next(); + const curNode = this.cursor.getCurNode(); + assert.equal( + curNode.getLocation(), + this.blocks.B.getInput('NAME4').connection, + ); + }); - test('In - From output connection', function () { - const fieldBlock = this.blocks.E; - const outputNode = ASTNode.createConnectionNode( - fieldBlock.outputConnection, - ); - this.cursor.setCurNode(outputNode); - this.cursor.in(); - const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), fieldBlock); - }); + test('In - From output connection', function () { + const fieldBlock = this.blocks.E; + const outputNode = ASTNode.createConnectionNode( + fieldBlock.outputConnection, + ); + this.cursor.setCurNode(outputNode); + this.cursor.in(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode.getLocation(), fieldBlock); + }); + + test('Prev - From previous connection does not skip over next connection', function () { + const prevConnection = this.blocks.B.previousConnection; + const prevConnectionNode = ASTNode.createConnectionNode(prevConnection); + this.cursor.setCurNode(prevConnectionNode); + this.cursor.prev(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode.getLocation(), this.blocks.A.nextConnection); + }); - test('Prev - From previous connection does not skip over next connection', function () { - const prevConnection = this.blocks.B.previousConnection; - const prevConnectionNode = ASTNode.createConnectionNode(prevConnection); - this.cursor.setCurNode(prevConnectionNode); - this.cursor.prev(); - const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.A.nextConnection); + test('Out - From field does not skip over block node', function () { + const field = this.blocks.E.inputList[0].fieldRow[0]; + const fieldNode = ASTNode.createFieldNode(field); + this.cursor.setCurNode(fieldNode); + this.cursor.out(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode.getLocation(), this.blocks.E); + }); }); + suite('Searching', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'empty_block', + 'message0': '', + }, + { + 'type': 'stack_block', + 'message0': '', + 'previousStatement': null, + 'nextStatement': null, + }, + { + 'type': 'row_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'input_value', + 'name': 'INPUT', + }, + ], + 'output': null, + }, + { + 'type': 'statement_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'input_statement', + 'name': 'STATEMENT', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + }, + { + 'type': 'c_hat_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'input_statement', + 'name': 'STATEMENT', + }, + ], + }, + ]); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.cursor = this.workspace.getCursor(); + }); + teardown(function () { + sharedTestTeardown.call(this); + }); + suite('one empty block', function () { + setup(function () { + this.blockA = this.workspace.newBlock('empty_block'); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + assert.equal(node.getLocation(), this.blockA); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + assert.equal(node.getLocation(), this.blockA); + }); + }); + + suite('one stack block', function () { + setup(function () { + this.blockA = this.workspace.newBlock('stack_block'); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + assert.equal(node.getLocation(), this.blockA.previousConnection); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + assert.equal(node.getLocation(), this.blockA.nextConnection); + }); + }); + + suite('one row block', function () { + setup(function () { + this.blockA = this.workspace.newBlock('row_block'); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + assert.equal(node.getLocation(), this.blockA.outputConnection); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + assert.equal(node.getLocation(), this.blockA.inputList[0].connection); + }); + }); + suite('one c-hat block', function () { + setup(function () { + this.blockA = this.workspace.newBlock('c_hat_block'); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + assert.equal(node.getLocation(), this.blockA); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + assert.equal(node.getLocation(), this.blockA.inputList[0].connection); + }); + }); - test('Out - From field does not skip over block node', function () { - const field = this.blocks.E.inputList[0].fieldRow[0]; - const fieldNode = ASTNode.createFieldNode(field); - this.cursor.setCurNode(fieldNode); - this.cursor.out(); - const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.E); + suite('multiblock stack', function () { + setup(function () { + const state = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'stack_block', + 'id': 'A', + 'x': 0, + 'y': 0, + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'B', + }, + }, + }, + ], + }, + }; + Blockly.serialization.workspaces.load(state, this.workspace); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + const blockA = this.workspace.getBlockById('A'); + assert.equal(node.getLocation(), blockA.previousConnection); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + const blockB = this.workspace.getBlockById('B'); + assert.equal(node.getLocation(), blockB.nextConnection); + }); + }); + + suite('multiblock row', function () { + setup(function () { + const state = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'row_block', + 'id': 'A', + 'x': 0, + 'y': 0, + 'inputs': { + 'INPUT': { + 'block': { + 'type': 'row_block', + 'id': 'B', + }, + }, + }, + }, + ], + }, + }; + Blockly.serialization.workspaces.load(state, this.workspace); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + const blockA = this.workspace.getBlockById('A'); + assert.equal(node.getLocation(), blockA.outputConnection); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + const blockB = this.workspace.getBlockById('B'); + assert.equal(node.getLocation(), blockB.inputList[0].connection); + }); + }); + suite('two stacks', function () { + setup(function () { + const state = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'stack_block', + 'id': 'A', + 'x': 0, + 'y': 0, + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'B', + }, + }, + }, + { + 'type': 'stack_block', + 'id': 'C', + 'x': 100, + 'y': 100, + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'D', + }, + }, + }, + ], + }, + }; + Blockly.serialization.workspaces.load(state, this.workspace); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + const location = node.getLocation(); + const previousConnection = + this.workspace.getBlockById('A').previousConnection; + assert.equal(location, previousConnection); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + const location = node.getLocation(); + const nextConnection = this.workspace.getBlockById('D').nextConnection; + assert.equal(location, nextConnection); + }); + }); }); }); From d1dc38f582cdd6235f4983055a6525aaeb4acf1d Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Fri, 11 Apr 2025 15:10:05 -0700 Subject: [PATCH 130/222] feat: support menuOpenEvent, menuSelectEvent, location for context menu items (#8877) * feat: support menuOpenEvent, menuSelectEvent, location for context menu items * feat: show context menu based on location * fix: rtl --- core/block_svg.ts | 53 ++++++++++++++++++--- core/comments/rendered_workspace_comment.ts | 25 +++++++++- core/contextmenu.ts | 52 +++++++++++++------- core/contextmenu_items.ts | 9 +++- core/contextmenu_registry.ts | 30 ++++++++---- core/menu.ts | 4 +- core/menuitem.ts | 12 +++-- core/workspace_svg.ts | 13 ++++- 8 files changed, 156 insertions(+), 42 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index ca753fc0a4e..18751eb133e 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -581,15 +581,16 @@ export class BlockSvg * * @returns Context menu options or null if no menu. */ - protected generateContextMenu(): Array< - ContextMenuOption | LegacyContextMenuOption - > | null { + protected generateContextMenu( + e: Event, + ): Array | null { if (this.workspace.isReadOnly() || !this.contextMenu) { return null; } const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( ContextMenuRegistry.ScopeType.BLOCK, {block: this}, + e, ); // Allow the block to add or modify menuOptions. @@ -600,17 +601,57 @@ export class BlockSvg return menuOptions; } + /** + * Gets the location in which to show the context menu for this block. + * Use the location of a click if the block was clicked, or a location + * based on the block's fields otherwise. + */ + protected calculateContextMenuLocation(e: Event): Coordinate { + // Open the menu where the user clicked, if they clicked + if (e instanceof PointerEvent) { + return new Coordinate(e.clientX, e.clientY); + } + + // Otherwise, calculate a location. + // Get the location of the top-left corner of the block in + // screen coordinates. + const blockCoords = svgMath.wsToScreenCoordinates( + this.workspace, + this.getRelativeToSurfaceXY(), + ); + + // Prefer a y position below the first field in the block. + const fieldBoundingClientRect = this.inputList + .filter((input) => input.isVisible()) + .flatMap((input) => input.fieldRow) + .find((f) => f.isVisible()) + ?.getSvgRoot() + ?.getBoundingClientRect(); + + const y = + fieldBoundingClientRect && fieldBoundingClientRect.height + ? fieldBoundingClientRect.y + fieldBoundingClientRect.height + : blockCoords.y + this.height; + + return new Coordinate( + this.RTL ? blockCoords.x - 5 : blockCoords.x + 5, + y + 5, + ); + } + /** * Show the context menu for this block. * * @param e Mouse event. * @internal */ - showContextMenu(e: PointerEvent) { - const menuOptions = this.generateContextMenu(); + showContextMenu(e: Event) { + const menuOptions = this.generateContextMenu(e); + + const location = this.calculateContextMenuLocation(e); if (menuOptions && menuOptions.length) { - ContextMenu.show(e, menuOptions, this.RTL, this.workspace); + ContextMenu.show(e, menuOptions, this.RTL, this.workspace, location); ContextMenu.setCurrentBlock(this); } } diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index 22525eb6a6b..9e48db0e45f 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -22,6 +22,7 @@ import {IRenderedElement} from '../interfaces/i_rendered_element.js'; import {ISelectable} from '../interfaces/i_selectable.js'; import * as layers from '../layers.js'; import * as commentSerialization from '../serialization/workspace_comments.js'; +import {svgMath} from '../utils.js'; import {Coordinate} from '../utils/coordinate.js'; import * as dom from '../utils/dom.js'; import {Rect} from '../utils/rect.js'; @@ -283,12 +284,32 @@ export class RenderedWorkspaceComment } /** Show a context menu for this comment. */ - showContextMenu(e: PointerEvent): void { + showContextMenu(e: Event): void { const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( ContextMenuRegistry.ScopeType.COMMENT, {comment: this}, + e, + ); + + let location: Coordinate; + if (e instanceof PointerEvent) { + location = new Coordinate(e.clientX, e.clientY); + } else { + // Show the menu based on the location of the comment + const xy = svgMath.wsToScreenCoordinates( + this.workspace, + this.getRelativeToSurfaceXY(), + ); + location = xy.translate(10, 10); + } + + contextMenu.show( + e, + menuOptions, + this.workspace.RTL, + this.workspace, + location, ); - contextMenu.show(e, menuOptions, this.workspace.RTL, this.workspace); } /** Snap this comment to the nearest grid point. */ diff --git a/core/contextmenu.ts b/core/contextmenu.ts index 7123198c2d8..4a83b9dccb5 100644 --- a/core/contextmenu.ts +++ b/core/contextmenu.ts @@ -21,6 +21,7 @@ import {Menu} from './menu.js'; import {MenuSeparator} from './menu_separator.js'; import {MenuItem} from './menuitem.js'; import * as serializationBlocks from './serialization/blocks.js'; +import {Coordinate} from './utils.js'; import * as aria from './utils/aria.js'; import * as dom from './utils/dom.js'; import {Rect} from './utils/rect.js'; @@ -38,6 +39,8 @@ const dummyOwner = {}; /** * Gets the block the context menu is currently attached to. + * It is not recommended that you use this function; instead, + * use the scope object passed to the context menu callback. * * @returns The block the context menu is attached to. */ @@ -62,26 +65,38 @@ let menu_: Menu | null = null; /** * Construct the menu based on the list of options and show the menu. * - * @param e Mouse event. + * @param menuOpenEvent Event that caused the menu to open. * @param options Array of menu options. * @param rtl True if RTL, false if LTR. * @param workspace The workspace associated with the context menu, if any. + * @param location The screen coordinates at which to show the menu. */ export function show( - e: PointerEvent, + menuOpenEvent: Event, options: (ContextMenuOption | LegacyContextMenuOption)[], rtl: boolean, workspace?: WorkspaceSvg, + location?: Coordinate, ) { WidgetDiv.show(dummyOwner, rtl, dispose, workspace); if (!options.length) { hide(); return; } - const menu = populate_(options, rtl, e); + + if (!location) { + if (menuOpenEvent instanceof PointerEvent) { + location = new Coordinate(menuOpenEvent.clientX, menuOpenEvent.clientY); + } else { + // We got a keyboard event that didn't tell us where to open the menu, so just guess + console.warn('Context menu opened with keyboard but no location given'); + location = new Coordinate(0, 0); + } + } + const menu = populate_(options, rtl, menuOpenEvent, location); menu_ = menu; - position_(menu, e, rtl); + position_(menu, rtl, location); // 1ms delay is required for focusing on context menus because some other // mouse event is still waiting in the queue and clears focus. setTimeout(function () { @@ -95,13 +110,15 @@ export function show( * * @param options Array of menu options. * @param rtl True if RTL, false if LTR. - * @param e The event that triggered the context menu to open. + * @param menuOpenEvent The event that triggered the context menu to open. + * @param location The screen coordinates at which to show the menu. * @returns The menu that will be shown on right click. */ function populate_( options: (ContextMenuOption | LegacyContextMenuOption)[], rtl: boolean, - e: PointerEvent, + menuOpenEvent: Event, + location: Coordinate, ): Menu { /* Here's what one option object looks like: {text: 'Make It So', @@ -123,7 +140,7 @@ function populate_( menu.addChild(menuItem); menuItem.setEnabled(option.enabled); if (option.enabled) { - const actionHandler = function () { + const actionHandler = function (p1: MenuItem, menuSelectEvent: Event) { hide(); requestAnimationFrame(() => { setTimeout(() => { @@ -131,7 +148,12 @@ function populate_( // will not be expecting a scope parameter, so there should be // no problems. Just assume it is a ContextMenuOption and we'll // pass undefined if it's not. - option.callback((option as ContextMenuOption).scope, e); + option.callback( + (option as ContextMenuOption).scope, + menuOpenEvent, + menuSelectEvent, + location, + ); }, 0); }); }; @@ -145,21 +167,19 @@ function populate_( * Add the menu to the page and position it correctly. * * @param menu The menu to add and position. - * @param e Mouse event for the right click that is making the context - * menu appear. * @param rtl True if RTL, false if LTR. + * @param location The location at which to anchor the menu. */ -function position_(menu: Menu, e: Event, rtl: boolean) { +function position_(menu: Menu, rtl: boolean, location: Coordinate) { // Record windowSize and scrollOffset before adding menu. const viewportBBox = svgMath.getViewportBBox(); - const mouseEvent = e as MouseEvent; // This one is just a point, but we'll pretend that it's a rect so we can use // some helper functions. const anchorBBox = new Rect( - mouseEvent.clientY + viewportBBox.top, - mouseEvent.clientY + viewportBBox.top, - mouseEvent.clientX + viewportBBox.left, - mouseEvent.clientX + viewportBBox.left, + location.y + viewportBBox.top, + location.y + viewportBBox.top, + location.x + viewportBBox.left, + location.x + viewportBBox.left, ); createWidget_(menu); diff --git a/core/contextmenu_items.ts b/core/contextmenu_items.ts index ebee8e3afc1..267305e2121 100644 --- a/core/contextmenu_items.ts +++ b/core/contextmenu_items.ts @@ -614,7 +614,12 @@ export function registerCommentCreate() { preconditionFn: (scope: Scope) => { return scope.workspace?.isMutator ? 'hidden' : 'enabled'; }, - callback: (scope: Scope, e: PointerEvent) => { + callback: ( + scope: Scope, + menuOpenEvent: Event, + menuSelectEvent: Event, + location: Coordinate, + ) => { const workspace = scope.workspace; if (!workspace) return; eventUtils.setGroup(true); @@ -622,7 +627,7 @@ export function registerCommentCreate() { comment.setPlaceholderText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']); comment.moveTo( pixelsToWorkspaceCoords( - new Coordinate(e.clientX, e.clientY), + new Coordinate(location.x, location.y), workspace, ), ); diff --git a/core/contextmenu_registry.ts b/core/contextmenu_registry.ts index 5bfb1eb63ec..f48fdfc679c 100644 --- a/core/contextmenu_registry.ts +++ b/core/contextmenu_registry.ts @@ -13,6 +13,7 @@ import type {BlockSvg} from './block_svg.js'; import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; +import {Coordinate} from './utils.js'; import type {WorkspaceSvg} from './workspace_svg.js'; /** @@ -83,6 +84,7 @@ export class ContextMenuRegistry { getContextMenuOptions( scopeType: ScopeType, scope: Scope, + menuOpenEvent: Event, ): ContextMenuOption[] { const menuOptions: ContextMenuOption[] = []; for (const item of this.registeredItems.values()) { @@ -102,7 +104,7 @@ export class ContextMenuRegistry { separator: true, }; } else { - const precondition = item.preconditionFn(scope); + const precondition = item.preconditionFn(scope, menuOpenEvent); if (precondition === 'hidden') continue; const displayText = @@ -165,12 +167,18 @@ export namespace ContextMenuRegistry { /** * @param scope Object that provides a reference to the thing that had its * context menu opened. - * @param e The original event that triggered the context menu to open. Not - * the event that triggered the click on the option. + * @param menuOpenEvent The original event that triggered the context menu to open. + * @param menuSelectEvent The event that triggered the option being selected. + * @param location The location in screen coordinates where the menu was opened. */ - callback: (scope: Scope, e: PointerEvent) => void; + callback: ( + scope: Scope, + menuOpenEvent: Event, + menuSelectEvent: Event, + location: Coordinate, + ) => void; displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement; - preconditionFn: (p1: Scope) => string; + preconditionFn: (p1: Scope, menuOpenEvent: Event) => string; separator?: never; } @@ -206,10 +214,16 @@ export namespace ContextMenuRegistry { /** * @param scope Object that provides a reference to the thing that had its * context menu opened. - * @param e The original event that triggered the context menu to open. Not - * the event that triggered the click on the option. + * @param menuOpenEvent The original event that triggered the context menu to open. + * @param menuSelectEvent The event that triggered the option being selected. + * @param location The location in screen coordinates where the menu was opened. */ - callback: (scope: Scope, e: PointerEvent) => void; + callback: ( + scope: Scope, + menuOpenEvent: Event, + menuSelectEvent: Event, + location: Coordinate, + ) => void; separator?: never; } diff --git a/core/menu.ts b/core/menu.ts index 3af474ee70b..664d3872d76 100644 --- a/core/menu.ts +++ b/core/menu.ts @@ -379,7 +379,7 @@ export class Menu { const menuItem = this.getMenuItem(e.target as Element); if (menuItem) { - menuItem.performAction(); + menuItem.performAction(e); } } @@ -431,7 +431,7 @@ export class Menu { case 'Enter': case ' ': if (highlighted) { - highlighted.performAction(); + highlighted.performAction(e); } break; diff --git a/core/menuitem.ts b/core/menuitem.ts index ebeb9404bdd..b3ae33c5c12 100644 --- a/core/menuitem.ts +++ b/core/menuitem.ts @@ -41,7 +41,8 @@ export class MenuItem { private highlight = false; /** Bound function to call when this menu item is clicked. */ - private actionHandler: ((obj: this) => void) | null = null; + private actionHandler: ((obj: this, menuSelectEvent: Event) => void) | null = + null; /** * @param content Text caption to display as the content of the item, or a @@ -220,11 +221,14 @@ export class MenuItem { * Performs the appropriate action when the menu item is activated * by the user. * + * @param menuSelectEvent the event that triggered the selection + * of the menu item. + * * @internal */ - performAction() { + performAction(menuSelectEvent: Event) { if (this.isEnabled() && this.actionHandler) { - this.actionHandler(this); + this.actionHandler(this, menuSelectEvent); } } @@ -236,7 +240,7 @@ export class MenuItem { * @param obj Used as the 'this' object in fn when called. * @internal */ - onAction(fn: (p1: MenuItem) => void, obj: object) { + onAction(fn: (p1: MenuItem, menuSelectEvent: Event) => void, obj: object) { this.actionHandler = fn.bind(obj); } } diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 7dcd5b5700f..e9414dcdeca 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -1706,13 +1706,14 @@ export class WorkspaceSvg * @param e Mouse event. * @internal */ - showContextMenu(e: PointerEvent) { + showContextMenu(e: Event) { if (this.isReadOnly() || this.isFlyout) { return; } const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( ContextMenuRegistry.ScopeType.WORKSPACE, {workspace: this}, + e, ); // Allow the developer to add or modify menuOptions. @@ -1720,7 +1721,15 @@ export class WorkspaceSvg this.configureContextMenu(menuOptions, e); } - ContextMenu.show(e, menuOptions, this.RTL, this); + let location; + if (e instanceof PointerEvent) { + location = new Coordinate(e.clientX, e.clientY); + } else { + // TODO: Get the location based on the workspace cursor location + location = svgMath.wsToScreenCoordinates(this, new Coordinate(5, 5)); + } + + ContextMenu.show(e, menuOptions, this.RTL, this, location); } /** From 3aa653d00b3d7480b334081e0803447f20a4b614 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 09:35:50 +0000 Subject: [PATCH 131/222] chore(deps): bump webdriverio from 9.4.2 to 9.12.5 Bumps [webdriverio](https://github.com/webdriverio/webdriverio/tree/HEAD/packages/webdriverio) from 9.4.2 to 9.12.5. - [Release notes](https://github.com/webdriverio/webdriverio/releases) - [Changelog](https://github.com/webdriverio/webdriverio/blob/main/CHANGELOG.md) - [Commits](https://github.com/webdriverio/webdriverio/commits/v9.12.5/packages/webdriverio) --- updated-dependencies: - dependency-name: webdriverio dependency-version: 9.12.5 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 418 +++++++++++++++++++++++++--------------------- 1 file changed, 230 insertions(+), 188 deletions(-) diff --git a/package-lock.json b/package-lock.json index d4a46c28a0d..d1f993c0ea9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1078,23 +1078,24 @@ "url": "https://github.com/webgptorg/promptbook/blob/main/README.md#%EF%B8%8F-contributing" } ], + "license": "CC-BY-4.0", "dependencies": { "spacetrim": "0.11.59" } }, "node_modules/@puppeteer/browsers": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", - "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.0.tgz", + "integrity": "sha512-HdHF4rny4JCvIcm7V1dpvpctIGqM3/Me255CB44vW7hDG1zYMmcBMjpNqZEDxdCfXGLkx5kP0+Jz5DUS+ukqtA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", - "semver": "^7.6.3", - "tar-fs": "^3.0.6", - "unbzip2-stream": "^1.4.3", + "semver": "^7.7.1", + "tar-fs": "^3.0.8", "yargs": "^17.7.2" }, "bin": { @@ -1105,10 +1106,11 @@ } }, "node_modules/@puppeteer/browsers/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1287,7 +1289,8 @@ "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@ts-stack/markdown": { "version": "1.4.0", @@ -1357,13 +1360,15 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -1373,6 +1378,7 @@ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" @@ -1611,15 +1617,15 @@ } }, "node_modules/@wdio/config": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.4.2.tgz", - "integrity": "sha512-pTwxN0EmkHzYKTfYnk/d+qIBRJFrNxNa58PDPbAc0On4dq7qqueDm/HyyFnTXc6t0vrqCNlR55DmKwqt3UqsKQ==", + "version": "9.12.5", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.12.5.tgz", + "integrity": "sha512-T4pOgY7FLj0+SBc58n81JZidCJKfqaSb9Ql9lOd38tmorEwTKjcPAzQQY1Ftzqv49kjBHvXdlupy685VVKNepA==", "dev": true, + "license": "MIT", "dependencies": { - "@wdio/logger": "9.1.3", - "@wdio/types": "9.4.2", - "@wdio/utils": "9.4.2", - "decamelize": "^6.0.0", + "@wdio/logger": "9.4.4", + "@wdio/types": "9.12.3", + "@wdio/utils": "9.12.5", "deepmerge-ts": "^7.0.3", "glob": "^10.2.2", "import-meta-resolve": "^4.0.0" @@ -1716,10 +1722,11 @@ } }, "node_modules/@wdio/logger": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.1.3.tgz", - "integrity": "sha512-cumRMK/gE1uedBUw3WmWXOQ7HtB6DR8EyKQioUz2P0IJtRRpglMBdZV7Svr3b++WWawOuzZHMfbTkJQmaVt8Gw==", + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.4.4.tgz", + "integrity": "sha512-BXx8RXFUW2M4dcO6t5Le95Hi2ZkTQBRsvBQqLekT2rZ6Xmw8ZKZBPf0FptnoftFGg6dYmwnDidYv/0+4PiHjpQ==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^5.1.2", "loglevel": "^1.6.0", @@ -1731,10 +1738,11 @@ } }, "node_modules/@wdio/logger/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -1747,6 +1755,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1758,16 +1767,18 @@ } }, "node_modules/@wdio/protocols": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.2.2.tgz", - "integrity": "sha512-0GMUSHCbYm+J+rnRU6XPtaUgVCRICsiH6W5zCXpePm3wLlbmg/mvZ+4OnNErssbpIOulZuAmC2jNmut2AEfWSw==", - "dev": true + "version": "9.12.5", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.12.5.tgz", + "integrity": "sha512-i+yc0EZtZOh5fFuwHxvcnXeTXk2ZjFICRbcAxTNE0F2Jr4uOydvcAOw4EIIRmb9NWUSPf/bGZAA+4SEXmxmjUA==", + "dev": true, + "license": "MIT" }, "node_modules/@wdio/repl": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.0.8.tgz", - "integrity": "sha512-3iubjl4JX5zD21aFxZwQghqC3lgu+mSs8c3NaiYYNCC+IT5cI/8QuKlgh9s59bu+N3gG988jqMJeCYlKuUv/iw==", + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.4.4.tgz", + "integrity": "sha512-kchPRhoG/pCn4KhHGiL/ocNhdpR8OkD2e6sANlSUZ4TGBVi86YSIEjc2yXUwLacHknC/EnQk/SFnqd4MsNjGGg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "^20.1.0" }, @@ -1776,10 +1787,11 @@ } }, "node_modules/@wdio/types": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.4.2.tgz", - "integrity": "sha512-eaEqngtnYQ2D6YLbfM2HGJVbJBjO1fAotPdu1G1cr28RAjcsANpl5IbDHZMlsKmiU9JtJx5CadhxCnrCLZVVDw==", + "version": "9.12.3", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.12.3.tgz", + "integrity": "sha512-MlnQ3WG1CQAjmUmeKtv3timGR91hSsCwQW9T1kqpu0VaJ/qbw3sWgtArMqRvgWB2H6IGueqQwDQ9qHlP013w9Q==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "^20.1.0" }, @@ -1788,14 +1800,15 @@ } }, "node_modules/@wdio/utils": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.4.2.tgz", - "integrity": "sha512-ryjPjmyR0s/9qgBBLKvh1cP/OndJ2BkY2LZhc4Nx6z9IxKmKDq79ajKNmsWR55mTMcembhcb8ikCACW8tEGEdA==", + "version": "9.12.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.12.5.tgz", + "integrity": "sha512-yddJj7VyA3kGuAuDU63ZdRBK4D1jwSU+52KwlZtOeqDdT/i6KAwRVYNYMwwmsGuM4GpY3q5h944YylBQNkKkjQ==", "dev": true, + "license": "MIT", "dependencies": { "@puppeteer/browsers": "^2.2.0", - "@wdio/logger": "9.1.3", - "@wdio/types": "9.4.2", + "@wdio/logger": "9.4.4", + "@wdio/types": "9.12.3", "decamelize": "^6.0.0", "deepmerge-ts": "^7.0.3", "edgedriver": "^6.1.1", @@ -1818,10 +1831,11 @@ "dev": true }, "node_modules/@zip.js/zip.js": { - "version": "2.7.54", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.54.tgz", - "integrity": "sha512-qMrJVg2hoEsZJjMJez9yI2+nZlBUxgYzGV3mqcb2B/6T1ihXp0fWBDYlVHlHquuorgNUQP5a8qSmX6HF5rFJNg==", + "version": "2.7.60", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.60.tgz", + "integrity": "sha512-vA3rLyqdxBrVo1FWSsbyoecaqWTV+vgPRf0QKeM7kVDG0r+lHUqd7zQDv1TO9k4BcAoNzNDSNrrel24Mk6addA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "bun": ">=0.7.0", "deno": ">=1.0.0", @@ -2368,6 +2382,7 @@ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.0.1" }, @@ -2379,7 +2394,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true + "dev": true, + "license": "0BSD" }, "node_modules/async": { "version": "3.2.5", @@ -2466,49 +2482,80 @@ "dev": true }, "node_modules/bare-events": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", - "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", "dev": true, + "license": "Apache-2.0", "optional": true }, "node_modules/bare-fs": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz", - "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.2.tgz", + "integrity": "sha512-8wSeOia5B7LwD4+h465y73KOdj5QHsbbuoUfPBi+pXgFJIPuG7SsiOdJuijWMyfid49eD+WivpfY7KT8gbAzBA==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "bare-events": "^2.0.0", - "bare-path": "^2.0.0", - "bare-stream": "^2.0.0" + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } } }, "node_modules/bare-os": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz", - "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", "dev": true, - "optional": true + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } }, "node_modules/bare-path": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", - "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "bare-os": "^2.1.0" + "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.1.tgz", - "integrity": "sha512-eVZbtKM+4uehzrsj49KtCy3Pbg7kO1pJ3SKZ1SFrIH/0pnj9scuGGgUlNDf/7qS8WKtGdiJY5Kyhs/ivYPTB/g==", + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } } }, "node_modules/base64-js": { @@ -2548,6 +2595,7 @@ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" } @@ -2642,35 +2690,12 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -2930,6 +2955,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || >=14" } @@ -3368,6 +3394,7 @@ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12" } @@ -3448,6 +3475,7 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -3485,10 +3513,11 @@ "dev": true }, "node_modules/deepmerge-ts": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.3.tgz", - "integrity": "sha512-qCSH6I0INPxd9Y1VtAiLpnYvz5O//6rCfJXKk0z66Up9/VOSr+1yS8XSKA5IWRxjocFGlzPyaZYe+jxq7OOLtQ==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=16.0.0" } @@ -3498,6 +3527,7 @@ "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", "dev": true, + "license": "MIT", "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", @@ -3631,6 +3661,7 @@ "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", "dev": true, + "license": "MIT", "dependencies": { "@types/which": "^2.0.1", "which": "^2.0.2" @@ -3648,6 +3679,7 @@ "integrity": "sha512-/dM/PoBf22Xg3yypMWkmRQrBKEnSyNaZ7wHGCT9+qqT14izwtFT+QvdR89rjNkMfXwW+bSFoqOfbcvM+2Cyc7w==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@wdio/logger": "^9.1.3", "@zip.js/zip.js": "^2.7.53", @@ -3671,6 +3703,7 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } @@ -3680,6 +3713,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -3817,6 +3851,7 @@ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -3838,6 +3873,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" @@ -4231,6 +4267,7 @@ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -4310,22 +4347,19 @@ "dev": true }, "node_modules/fast-xml-parser": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz", - "integrity": "sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" } ], + "license": "MIT", "dependencies": { - "strnum": "^1.0.5" + "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" @@ -4354,6 +4388,7 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, + "license": "MIT", "dependencies": { "pend": "~1.2.0" } @@ -4373,6 +4408,7 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -4573,6 +4609,7 @@ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "dev": true, + "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" }, @@ -4646,6 +4683,7 @@ "integrity": "sha512-vn7TtQ3b9VMJtVXsyWtQQl1fyBVFhQy7UvJF96kPuuJ0or5THH496AD3eUyaDD11+EqCxH9t6V+EP9soZQk4YQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@wdio/logger": "^9.1.3", "@zip.js/zip.js": "^2.7.53", @@ -4668,6 +4706,7 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } @@ -4677,6 +4716,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -4710,6 +4750,7 @@ "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -4722,6 +4763,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -4737,6 +4779,7 @@ "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", "dev": true, + "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", @@ -4751,6 +4794,7 @@ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14" } @@ -5497,10 +5541,11 @@ } }, "node_modules/htmlfy": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.3.2.tgz", - "integrity": "sha512-FsxzfpeDYRqn1emox9VpxMPfGjADoUmmup8D604q497R0VNxiXs4ZZTN2QzkaMA5C9aHGUoe1iQRVSm+HK9xuA==", - "dev": true + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.6.7.tgz", + "integrity": "sha512-r8hRd+oIM10lufovN+zr3VKPTYEIvIwqXGucidh2XQufmiw6sbUXFUFjWlfjo3AnefIDTyzykVzQ8IUVuT1peQ==", + "dev": true, + "license": "MIT" }, "node_modules/htmlparser2": { "version": "9.1.0", @@ -5663,6 +5708,7 @@ "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", "dev": true, + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5713,6 +5759,7 @@ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dev": true, + "license": "MIT", "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" @@ -5725,7 +5772,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/is-absolute": { "version": "1.0.0", @@ -6024,7 +6072,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.1.0", @@ -6349,6 +6398,7 @@ "url": "https://github.com/hejny/locate-app/blob/main/README.md#%EF%B8%8F-contributing" } ], + "license": "Apache-2.0", "dependencies": { "@promptbook/utils": "0.69.5", "type-fest": "4.26.0", @@ -6360,6 +6410,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, @@ -6464,6 +6515,7 @@ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" }, @@ -6476,7 +6528,8 @@ "version": "0.8.4", "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/loupe": { "version": "3.1.1", @@ -6798,6 +6851,7 @@ "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4.0" } @@ -6836,6 +6890,7 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "engines": { "node": ">=10.5.0" } @@ -6845,6 +6900,7 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dev": true, + "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -7023,10 +7079,11 @@ } }, "node_modules/pac-proxy-agent": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", - "integrity": "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", "dev": true, + "license": "MIT", "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", @@ -7046,6 +7103,7 @@ "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, + "license": "MIT", "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" @@ -7356,7 +7414,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/picocolors": { "version": "0.2.1", @@ -7534,6 +7593,7 @@ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -7543,6 +7603,7 @@ "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -7562,6 +7623,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -7570,13 +7632,15 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dev": true, + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -8035,6 +8099,7 @@ "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-1.0.0.tgz", "integrity": "sha512-J92IFbskyo7OYB3Dt4aTdyhag1GlInrfbPCmMteb7aBK7PwlnGz1HI0+oyNN97j7pV9DqUAVoVgkNRMrfY47mQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.0.0" } @@ -8204,16 +8269,18 @@ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "dev": true, + "license": "MIT", "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -8228,6 +8295,7 @@ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -8270,7 +8338,8 @@ "type": "github", "url": "https://github.com/hejny/spacetrim/blob/main/README.md#%EF%B8%8F-contributing" } - ] + ], + "license": "Apache-2.0" }, "node_modules/sparkles": { "version": "2.1.0", @@ -8298,6 +8367,7 @@ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "dev": true, + "license": "ISC", "engines": { "node": ">= 10.x" } @@ -8497,10 +8567,17 @@ } }, "node_modules/strnum": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" }, "node_modules/supports-color": { "version": "7.2.0", @@ -8573,17 +8650,18 @@ "dev": true }, "node_modules/tar-fs": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", - "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", + "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { - "bare-fs": "^2.1.1", - "bare-path": "^2.1.0" + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, "node_modules/tar-stream": { @@ -8627,12 +8705,6 @@ "url": "https://bevry.me/fund" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "node_modules/time-stamp": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", @@ -8824,16 +8896,6 @@ "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "dev": true, - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -8938,6 +9000,7 @@ "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.1.tgz", "integrity": "sha512-5cnLm4gseXjAclKowC4IjByaGsjtAoV6PrOQOljplNB54ReUYJP8HdAFq2muHinSDAh09PPX/uXDPfdxRHvuSA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -9135,6 +9198,7 @@ "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.2", "commander": "^9.3.0", @@ -9152,23 +9216,25 @@ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/webdriver": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.4.2.tgz", - "integrity": "sha512-1H3DY42kaGYF05sSozwtBpEYxJsnWga2QqYQAPwQmNonqJznGWkl5tlNEuqvI+T7pHTiLbOHlldgdMV5woSrqA==", + "version": "9.12.5", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.12.5.tgz", + "integrity": "sha512-CQCb1kDh52VtzPOIWc6XOdRz9q07LMAm9XwL+ABLSd0gueJq+GZoUTqHVX1YwVF0EQlFnw0JYJok0hxGH7m7cw==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "^20.1.0", "@types/ws": "^8.5.3", - "@wdio/config": "9.4.2", - "@wdio/logger": "9.1.3", - "@wdio/protocols": "9.2.2", - "@wdio/types": "9.4.2", - "@wdio/utils": "9.4.2", + "@wdio/config": "9.12.5", + "@wdio/logger": "9.4.4", + "@wdio/protocols": "9.12.5", + "@wdio/types": "9.12.3", + "@wdio/utils": "9.12.5", "deepmerge-ts": "^7.0.3", "undici": "^6.20.1", "ws": "^8.8.0" @@ -9178,44 +9244,43 @@ } }, "node_modules/webdriverio": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.4.2.tgz", - "integrity": "sha512-e6Gu5pFWQEtJuz3tT0NV3FRhC4/yZuWA8/Q7u1GGC+kaKxZOEuXxNMyrhfSstugGvQ92jnbWP/g2lBwhrZIl2A==", + "version": "9.12.5", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.12.5.tgz", + "integrity": "sha512-ho7gEOdPkpMlZJ5fbCX6+zAllnVdYl8X9RZ4x3tDabf3ByEzReqexaTVou8ayWmNngGjarWlXX3ov1BIdhQTLQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "^20.11.30", "@types/sinonjs__fake-timers": "^8.1.5", - "@wdio/config": "9.4.2", - "@wdio/logger": "9.1.3", - "@wdio/protocols": "9.2.2", - "@wdio/repl": "9.0.8", - "@wdio/types": "9.4.2", - "@wdio/utils": "9.4.2", + "@wdio/config": "9.12.5", + "@wdio/logger": "9.4.4", + "@wdio/protocols": "9.12.5", + "@wdio/repl": "9.4.4", + "@wdio/types": "9.12.3", + "@wdio/utils": "9.12.5", "archiver": "^7.0.1", "aria-query": "^5.3.0", "cheerio": "^1.0.0-rc.12", "css-shorthand-properties": "^1.1.1", "css-value": "^0.0.1", "grapheme-splitter": "^1.0.4", - "htmlfy": "^0.3.0", - "import-meta-resolve": "^4.0.0", + "htmlfy": "^0.6.0", "is-plain-obj": "^4.1.0", "jszip": "^3.10.1", "lodash.clonedeep": "^4.5.0", "lodash.zip": "^4.2.0", - "minimatch": "^9.0.3", "query-selector-shadow-dom": "^1.0.1", "resq": "^1.11.0", "rgb2hex": "0.2.5", "serialize-error": "^11.0.3", "urlpattern-polyfill": "^10.0.0", - "webdriver": "9.4.2" + "webdriver": "9.12.5" }, "engines": { "node": ">=18.20.0" }, "peerDependencies": { - "puppeteer-core": "^22.3.0" + "puppeteer-core": ">=22.x || <=24.x" }, "peerDependenciesMeta": { "puppeteer-core": { @@ -9223,15 +9288,6 @@ } } }, - "node_modules/webdriverio/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/webdriverio/node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -9244,21 +9300,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webdriverio/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -9510,6 +9551,7 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, + "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" From fac75043dde579206a3163c2a134337afcd687ae Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Mon, 14 Apr 2025 09:58:58 -0700 Subject: [PATCH 132/222] feat: add loopback in cursor navigation, and add tests (#8883) * chore: tests for cursor getNextNode * chore: add tests for getPreviousNode * feat: add looping to getPreviousNode and getNextNode * chore: inline returns * chore: fix test that results in a stack node * chore: fix annotations --- core/keyboard_nav/line_cursor.ts | 130 ++++++--- tests/mocha/cursor_test.js | 440 +++++++++++++++++++++++++++++++ 2 files changed, 528 insertions(+), 42 deletions(-) diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index d8e0a472bad..b2bda39c739 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -99,7 +99,11 @@ export class LineCursor extends Marker { if (!curNode) { return null; } - const newNode = this.getNextNode(curNode, this.validLineNode.bind(this)); + const newNode = this.getNextNode( + curNode, + this.validLineNode.bind(this), + true, + ); if (newNode) { this.setCurNode(newNode); @@ -119,7 +123,11 @@ export class LineCursor extends Marker { if (!curNode) { return null; } - const newNode = this.getNextNode(curNode, this.validInLineNode.bind(this)); + const newNode = this.getNextNode( + curNode, + this.validInLineNode.bind(this), + true, + ); if (newNode) { this.setCurNode(newNode); @@ -138,11 +146,10 @@ export class LineCursor extends Marker { if (!curNode) { return null; } - const newNode = this.getPreviousNode( + const newNode = this.getPreviousNodeImpl( curNode, this.validLineNode.bind(this), ); - if (newNode) { this.setCurNode(newNode); } @@ -161,7 +168,7 @@ export class LineCursor extends Marker { if (!curNode) { return null; } - const newNode = this.getPreviousNode( + const newNode = this.getPreviousNodeImpl( curNode, this.validInLineNode.bind(this), ); @@ -184,6 +191,7 @@ export class LineCursor extends Marker { const rightNode = this.getNextNode( curNode, this.validInLineNode.bind(this), + false, ); return this.validLineNode(rightNode); } @@ -299,28 +307,46 @@ export class LineCursor extends Marker { * should be traversed. * @returns The next node in the traversal. */ - getNextNode( + private getNextNodeImpl( node: ASTNode | null, isValid: (p1: ASTNode | null) => boolean, ): ASTNode | null { - if (!node) { - return null; - } - const newNode = node.in() || node.next(); - if (isValid(newNode)) { - return newNode; - } else if (newNode) { - return this.getNextNode(newNode, isValid); - } - const siblingOrParentSibling = this.findSiblingOrParentSibling(node.out()); - if (isValid(siblingOrParentSibling)) { - return siblingOrParentSibling; - } else if (siblingOrParentSibling) { - return this.getNextNode(siblingOrParentSibling, isValid); - } + if (!node) return null; + let newNode = node.in() || node.next(); + if (isValid(newNode)) return newNode; + if (newNode) return this.getNextNodeImpl(newNode, isValid); + + newNode = this.findSiblingOrParentSibling(node.out()); + if (isValid(newNode)) return newNode; + if (newNode) return this.getNextNodeImpl(newNode, isValid); return null; } + /** + * Get the next node in the AST, optionally allowing for loopback. + * + * @param node The current position in the AST. + * @param isValid A function true/false depending on whether the given node + * should be traversed. + * @param loop Whether to loop around to the beginning of the workspace if + * novalid node was found. + * @returns The next node in the traversal. + */ + getNextNode( + node: ASTNode | null, + isValid: (p1: ASTNode | null) => boolean, + loop: boolean, + ): ASTNode | null { + if (!node) return null; + + const potential = this.getNextNodeImpl(node, isValid); + if (potential || !loop) return potential; + // Loop back. + const firstNode = this.getFirstNode(); + if (isValid(firstNode)) return firstNode; + return this.getNextNodeImpl(firstNode, isValid); + } + /** * Reverses the pre order traversal in order to find the previous node. This * will allow a user to easily navigate the entire Blockly AST without having @@ -332,13 +358,11 @@ export class LineCursor extends Marker { * @returns The previous node in the traversal or null if no previous node * exists. */ - getPreviousNode( + private getPreviousNodeImpl( node: ASTNode | null, isValid: (p1: ASTNode | null) => boolean, ): ASTNode | null { - if (!node) { - return null; - } + if (!node) return null; let newNode: ASTNode | null = node.prev(); if (newNode) { @@ -346,14 +370,38 @@ export class LineCursor extends Marker { } else { newNode = node.out(); } - if (isValid(newNode)) { - return newNode; - } else if (newNode) { - return this.getPreviousNode(newNode, isValid); - } + + if (isValid(newNode)) return newNode; + if (newNode) return this.getPreviousNodeImpl(newNode, isValid); return null; } + /** + * Get the previous node in the AST, optionally allowing for loopback. + * + * @param node The current position in the AST. + * @param isValid A function true/false depending on whether the given node + * should be traversed. + * @param loop Whether to loop around to the end of the workspace if no + * valid node was found. + * @returns The previous node in the traversal or null if no previous node + * exists. + */ + getPreviousNode( + node: ASTNode | null, + isValid: (p1: ASTNode | null) => boolean, + loop: boolean, + ): ASTNode | null { + if (!node) return null; + + const potential = this.getPreviousNodeImpl(node, isValid); + if (potential || !loop) return potential; + // Loop back. + const lastNode = this.getLastNode(); + if (isValid(lastNode)) return lastNode; + return this.getPreviousNodeImpl(lastNode, isValid); + } + /** * From the given node find either the next valid sibling or the parent's * next sibling. @@ -362,13 +410,9 @@ export class LineCursor extends Marker { * @returns The next sibling node, the parent's next sibling, or null. */ private findSiblingOrParentSibling(node: ASTNode | null): ASTNode | null { - if (!node) { - return null; - } + if (!node) return null; const nextNode = node.next(); - if (nextNode) { - return nextNode; - } + if (nextNode) return nextNode; return this.findSiblingOrParentSibling(node.out()); } @@ -381,9 +425,7 @@ export class LineCursor extends Marker { */ private getRightMostChild(node: ASTNode): ASTNode | null { let newNode = node.in(); - if (!newNode) { - return node; - } + if (!newNode) return node; for ( let nextNode: ASTNode | null = newNode; nextNode; @@ -787,9 +829,13 @@ export class LineCursor extends Marker { // Iterate until you fall off the end of the stack. while (nextNode) { prevNode = nextNode; - nextNode = this.getNextNode(prevNode, (node) => { - return !!node; - }); + nextNode = this.getNextNode( + prevNode, + (node) => { + return !!node; + }, + false, + ); } return prevNode; } diff --git a/tests/mocha/cursor_test.js b/tests/mocha/cursor_test.js index b2a38268866..905f48c09ad 100644 --- a/tests/mocha/cursor_test.js +++ b/tests/mocha/cursor_test.js @@ -385,4 +385,444 @@ suite('Cursor', function () { }); }); }); + suite('Get next node', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'empty_block', + 'message0': '', + }, + { + 'type': 'stack_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_input', + 'name': 'FIELD', + 'text': 'default', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + }, + { + 'type': 'row_block', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'field_input', + 'name': 'FIELD', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'INPUT', + }, + ], + 'output': null, + }, + ]); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.cursor = this.workspace.getCursor(); + this.neverValid = () => false; + this.alwaysValid = () => true; + this.isConnection = (node) => { + return node && node.isConnection(); + }; + }); + teardown(function () { + sharedTestTeardown.call(this); + }); + suite('stack', function () { + setup(function () { + const state = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'stack_block', + 'id': 'A', + 'x': 0, + 'y': 0, + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'B', + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'C', + }, + }, + }, + }, + }, + ], + }, + }; + Blockly.serialization.workspaces.load(state, this.workspace); + this.blockA = this.workspace.getBlockById('A'); + this.blockB = this.workspace.getBlockById('B'); + this.blockC = this.workspace.getBlockById('C'); + }); + teardown(function () { + this.workspace.clear(); + }); + test('Never valid - start at top', function () { + const startNode = ASTNode.createConnectionNode( + this.blockA.previousConnection, + ); + const nextNode = this.cursor.getNextNode( + startNode, + this.neverValid, + false, + ); + assert.isNull(nextNode); + }); + test('Never valid - start in middle', function () { + const startNode = ASTNode.createBlockNode(this.blockB); + const nextNode = this.cursor.getNextNode( + startNode, + this.neverValid, + false, + ); + assert.isNull(nextNode); + }); + test('Never valid - start at end', function () { + const startNode = ASTNode.createConnectionNode( + this.blockC.nextConnection, + ); + const nextNode = this.cursor.getNextNode( + startNode, + this.neverValid, + false, + ); + assert.isNull(nextNode); + }); + + test('Always valid - start at top', function () { + const startNode = ASTNode.createConnectionNode( + this.blockA.previousConnection, + ); + const nextNode = this.cursor.getNextNode( + startNode, + this.alwaysValid, + false, + ); + assert.equal(nextNode.getLocation(), this.blockA); + }); + test('Always valid - start in middle', function () { + const startNode = ASTNode.createBlockNode(this.blockB); + const nextNode = this.cursor.getNextNode( + startNode, + this.alwaysValid, + false, + ); + assert.equal(nextNode.getLocation(), this.blockB.getField('FIELD')); + }); + test('Always valid - start at end', function () { + const startNode = ASTNode.createConnectionNode( + this.blockC.nextConnection, + ); + const nextNode = this.cursor.getNextNode( + startNode, + this.alwaysValid, + false, + ); + assert.isNull(nextNode); + }); + + test('Valid if connection - start at top', function () { + const startNode = ASTNode.createConnectionNode( + this.blockA.previousConnection, + ); + const nextNode = this.cursor.getNextNode( + startNode, + this.isConnection, + false, + ); + assert.equal(nextNode.getLocation(), this.blockA.nextConnection); + }); + test('Valid if connection - start in middle', function () { + const startNode = ASTNode.createBlockNode(this.blockB); + const nextNode = this.cursor.getNextNode( + startNode, + this.isConnection, + false, + ); + assert.equal(nextNode.getLocation(), this.blockB.nextConnection); + }); + test('Valid if connection - start at end', function () { + const startNode = ASTNode.createConnectionNode( + this.blockC.nextConnection, + ); + const nextNode = this.cursor.getNextNode( + startNode, + this.isConnection, + false, + ); + assert.isNull(nextNode); + }); + test('Never valid - start at end - with loopback', function () { + const startNode = ASTNode.createConnectionNode( + this.blockC.nextConnection, + ); + const nextNode = this.cursor.getNextNode( + startNode, + this.neverValid, + true, + ); + assert.isNull(nextNode); + }); + test('Always valid - start at end - with loopback', function () { + const startNode = ASTNode.createConnectionNode( + this.blockC.nextConnection, + ); + const nextNode = this.cursor.getNextNode( + startNode, + this.alwaysValid, + true, + ); + assert.equal(nextNode.getLocation(), this.blockA.previousConnection); + }); + + test('Valid if connection - start at end - with loopback', function () { + const startNode = ASTNode.createConnectionNode( + this.blockC.nextConnection, + ); + const nextNode = this.cursor.getNextNode( + startNode, + this.isConnection, + true, + ); + assert.equal(nextNode.getLocation(), this.blockA.previousConnection); + }); + }); + }); + + suite('Get previous node', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'empty_block', + 'message0': '', + }, + { + 'type': 'stack_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_input', + 'name': 'FIELD', + 'text': 'default', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + }, + { + 'type': 'row_block', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'field_input', + 'name': 'FIELD', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'INPUT', + }, + ], + 'output': null, + }, + ]); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.cursor = this.workspace.getCursor(); + this.neverValid = () => false; + this.alwaysValid = () => true; + this.isConnection = (node) => { + return node && node.isConnection(); + }; + }); + teardown(function () { + sharedTestTeardown.call(this); + }); + suite('stack', function () { + setup(function () { + const state = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'stack_block', + 'id': 'A', + 'x': 0, + 'y': 0, + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'B', + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'C', + }, + }, + }, + }, + }, + ], + }, + }; + Blockly.serialization.workspaces.load(state, this.workspace); + this.blockA = this.workspace.getBlockById('A'); + this.blockB = this.workspace.getBlockById('B'); + this.blockC = this.workspace.getBlockById('C'); + }); + teardown(function () { + this.workspace.clear(); + }); + test('Never valid - start at top', function () { + const startNode = ASTNode.createConnectionNode( + this.blockA.previousConnection, + ); + const previousNode = this.cursor.getPreviousNode( + startNode, + this.neverValid, + false, + ); + assert.isNull(previousNode); + }); + test('Never valid - start in middle', function () { + const startNode = ASTNode.createBlockNode(this.blockB); + const previousNode = this.cursor.getPreviousNode( + startNode, + this.neverValid, + false, + ); + assert.isNull(previousNode); + }); + test('Never valid - start at end', function () { + const startNode = ASTNode.createConnectionNode( + this.blockC.nextConnection, + ); + const previousNode = this.cursor.getPreviousNode( + startNode, + this.neverValid, + false, + ); + assert.isNull(previousNode); + }); + + test('Always valid - start at top', function () { + const startNode = ASTNode.createConnectionNode( + this.blockA.previousConnection, + ); + const previousNode = this.cursor.getPreviousNode( + startNode, + this.alwaysValid, + false, + ); + assert.isNotNull(previousNode); + }); + test('Always valid - start in middle', function () { + const startNode = ASTNode.createBlockNode(this.blockB); + const previousNode = this.cursor.getPreviousNode( + startNode, + this.alwaysValid, + false, + ); + assert.equal( + previousNode.getLocation(), + this.blockB.previousConnection, + ); + }); + test('Always valid - start at end', function () { + const startNode = ASTNode.createConnectionNode( + this.blockC.nextConnection, + ); + const previousNode = this.cursor.getPreviousNode( + startNode, + this.alwaysValid, + false, + ); + assert.equal(previousNode.getLocation(), this.blockC.getField('FIELD')); + }); + + test('Valid if connection - start at top', function () { + const startNode = ASTNode.createConnectionNode( + this.blockA.previousConnection, + ); + const previousNode = this.cursor.getPreviousNode( + startNode, + this.isConnection, + false, + ); + assert.isNull(previousNode); + }); + test('Valid if connection - start in middle', function () { + const startNode = ASTNode.createBlockNode(this.blockB); + const previousNode = this.cursor.getPreviousNode( + startNode, + this.isConnection, + false, + ); + assert.equal( + previousNode.getLocation(), + this.blockB.previousConnection, + ); + }); + test('Valid if connection - start at end', function () { + const startNode = ASTNode.createConnectionNode( + this.blockC.nextConnection, + ); + const previousNode = this.cursor.getPreviousNode( + startNode, + this.isConnection, + false, + ); + assert.equal( + previousNode.getLocation(), + this.blockC.previousConnection, + ); + }); + test('Never valid - start at top - with loopback', function () { + const startNode = ASTNode.createConnectionNode( + this.blockA.previousConnection, + ); + const previousNode = this.cursor.getPreviousNode( + startNode, + this.neverValid, + true, + ); + assert.isNull(previousNode); + }); + test('Always valid - start at top - with loopback', function () { + const startNode = ASTNode.createConnectionNode( + this.blockA.previousConnection, + ); + const previousNode = this.cursor.getPreviousNode( + startNode, + this.alwaysValid, + true, + ); + // Previous node will be a stack node in this case. + assert.equal(previousNode.getLocation(), this.blockA); + }); + test('Valid if connection - start at top - with loopback', function () { + const startNode = ASTNode.createConnectionNode( + this.blockA.previousConnection, + ); + const previousNode = this.cursor.getPreviousNode( + startNode, + this.isConnection, + true, + ); + assert.equal(previousNode.getLocation(), this.blockC.nextConnection); + }); + }); + }); }); From e0009e257c4e530cf01b34aa9a70845aab8252f5 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Mon, 14 Apr 2025 12:16:40 -0700 Subject: [PATCH 133/222] fix: update dependencies so adv compilation works (#8890) --- core/comments/rendered_workspace_comment.ts | 2 +- core/contextmenu.ts | 2 +- core/contextmenu_registry.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index 9e48db0e45f..8a592a78b3c 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -22,11 +22,11 @@ import {IRenderedElement} from '../interfaces/i_rendered_element.js'; import {ISelectable} from '../interfaces/i_selectable.js'; import * as layers from '../layers.js'; import * as commentSerialization from '../serialization/workspace_comments.js'; -import {svgMath} from '../utils.js'; import {Coordinate} from '../utils/coordinate.js'; import * as dom from '../utils/dom.js'; import {Rect} from '../utils/rect.js'; import {Size} from '../utils/size.js'; +import * as svgMath from '../utils/svg_math.js'; import {WorkspaceSvg} from '../workspace_svg.js'; import {CommentView} from './comment_view.js'; import {WorkspaceComment} from './workspace_comment.js'; diff --git a/core/contextmenu.ts b/core/contextmenu.ts index 4a83b9dccb5..4ba09de8231 100644 --- a/core/contextmenu.ts +++ b/core/contextmenu.ts @@ -21,8 +21,8 @@ import {Menu} from './menu.js'; import {MenuSeparator} from './menu_separator.js'; import {MenuItem} from './menuitem.js'; import * as serializationBlocks from './serialization/blocks.js'; -import {Coordinate} from './utils.js'; import * as aria from './utils/aria.js'; +import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import {Rect} from './utils/rect.js'; import * as svgMath from './utils/svg_math.js'; diff --git a/core/contextmenu_registry.ts b/core/contextmenu_registry.ts index f48fdfc679c..06b60801a0a 100644 --- a/core/contextmenu_registry.ts +++ b/core/contextmenu_registry.ts @@ -13,7 +13,7 @@ import type {BlockSvg} from './block_svg.js'; import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; -import {Coordinate} from './utils.js'; +import {Coordinate} from './utils/coordinate.js'; import type {WorkspaceSvg} from './workspace_svg.js'; /** From 7a3eb62142d4fa28b5adacf8f6324d988bf24013 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Apr 2025 13:20:33 -0700 Subject: [PATCH 134/222] fix: Don't visit invisible inputs with the cursor. (#8892) --- core/keyboard_nav/ast_node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts index 0de0ffb57f0..ced10209b95 100644 --- a/core/keyboard_nav/ast_node.ts +++ b/core/keyboard_nav/ast_node.ts @@ -468,7 +468,7 @@ export class ASTNode { return ASTNode.createFieldNode(field); } } - if (input.connection) { + if (input.connection && input.isVisible()) { return ASTNode.createInputNode(input); } } From 98cf5cb8eee4101eb1975573c6cca70f67e41f99 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Apr 2025 13:54:15 -0700 Subject: [PATCH 135/222] feat: Add support for retrieving blocks' drag strategies. (#8893) --- core/block_svg.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/block_svg.ts b/core/block_svg.ts index 18751eb133e..b2f6c1c9cf8 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -1743,6 +1743,16 @@ export class BlockSvg ); } + /** + * Returns the drag strategy currently in use by this block. + * + * @internal + * @returns This block's drag strategy. + */ + getDragStrategy(): IDragStrategy { + return this.dragStrategy; + } + /** Sets the drag strategy for this block. */ setDragStrategy(dragStrategy: IDragStrategy) { this.dragStrategy = dragStrategy; From e45471d6f4cd6a431d500e8a7d22837ef0caf37f Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Apr 2025 13:56:46 -0700 Subject: [PATCH 136/222] fix: Fix menu scrolling. (#8881) --- core/css.ts | 7 +++---- core/field_dropdown.ts | 20 +++++++++----------- core/menu.ts | 5 ++--- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/core/css.ts b/core/css.ts index 0502edbf13a..6ca262f3b25 100644 --- a/core/css.ts +++ b/core/css.ts @@ -124,9 +124,6 @@ let content = ` .blocklyDropDownContent { max-height: 300px; /* @todo: spec for maximum height. */ - overflow: auto; - overflow-x: hidden; - position: relative; } .blocklyDropDownArrow { @@ -416,7 +413,9 @@ input[type=number] { border: inherit; /* Compatibility with gapi, reset from goog-menu */ font: normal 13px "Helvetica Neue", Helvetica, sans-serif; outline: none; - position: relative; /* Compatibility with gapi, reset from goog-menu */ + overflow-y: auto; + overflow-x: hidden; + max-height: 100%; z-index: 20000; /* Arbitrary, but some apps depend on it... */ } diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 525dbf01b70..23a8a3f7da0 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -30,7 +30,6 @@ import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import * as parsing from './utils/parsing.js'; import * as utilsString from './utils/string.js'; -import * as style from './utils/style.js'; import {Svg} from './utils/svg.js'; /** @@ -276,16 +275,18 @@ export class FieldDropdown extends Field { throw new UnattachedFieldError(); } this.dropdownCreate(); + if (!this.menu_) return; + if (e && typeof e.clientX === 'number') { - this.menu_!.openingCoords = new Coordinate(e.clientX, e.clientY); + this.menu_.openingCoords = new Coordinate(e.clientX, e.clientY); } else { - this.menu_!.openingCoords = null; + this.menu_.openingCoords = null; } // Remove any pre-existing elements in the dropdown. dropDownDiv.clearContent(); // Element gets created in render. - const menuElement = this.menu_!.render(dropDownDiv.getContentDiv()); + const menuElement = this.menu_.render(dropDownDiv.getContentDiv()); dom.addClass(menuElement, 'blocklyDropdownMenu'); if (this.getConstants()!.FIELD_DROPDOWN_COLOURED_DIV) { @@ -296,18 +297,15 @@ export class FieldDropdown extends Field { dropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this)); + dropDownDiv.getContentDiv().style.height = `${this.menu_.getSize().height}px`; + // Focusing needs to be handled after the menu is rendered and positioned. // Otherwise it will cause a page scroll to get the misplaced menu in // view. See issue #1329. - this.menu_!.focus(); + this.menu_.focus(); if (this.selectedMenuItem) { - this.menu_!.setHighlighted(this.selectedMenuItem); - style.scrollIntoContainerView( - this.selectedMenuItem.getElement()!, - dropDownDiv.getContentDiv(), - true, - ); + this.menu_.setHighlighted(this.selectedMenuItem); } this.applyColour(); diff --git a/core/menu.ts b/core/menu.ts index 664d3872d76..13fd0866f49 100644 --- a/core/menu.ts +++ b/core/menu.ts @@ -258,11 +258,10 @@ export class Menu { // Bring the highlighted item into view. This has no effect if the menu is // not scrollable. const menuElement = this.getElement(); - const scrollingParent = menuElement?.parentElement; const menuItemElement = item.getElement(); - if (!scrollingParent || !menuItemElement) return; + if (!menuElement || !menuItemElement) return; - style.scrollIntoContainerView(menuItemElement, scrollingParent); + style.scrollIntoContainerView(menuItemElement, menuElement); aria.setState(menuElement, aria.State.ACTIVEDESCENDANT, item.getId()); } } From b1d7670a6eb6d8e8021069d0e713504f5aad4e95 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Apr 2025 15:09:07 -0700 Subject: [PATCH 137/222] release: Update version number to 12.0.0-beta.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e422e98683..c3f6b037677 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockly", - "version": "12.0.0-beta.3", + "version": "12.0.0-beta.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "12.0.0-beta.3", + "version": "12.0.0-beta.4", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index af6436abd0c..4cb3f225b66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "12.0.0-beta.3", + "version": "12.0.0-beta.4", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly" From acca9ea83fdd399f95880aabc01da6d9546956cc Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Tue, 15 Apr 2025 11:24:01 -0700 Subject: [PATCH 138/222] feat!: deprecate scopeType and include focusedNode in context menu options (#8882) * feat!: deprecate scopeType and include focusedNode in context menu options * chore: add issue to todo --- core/block_svg.ts | 3 +- core/comments/rendered_workspace_comment.ts | 3 +- core/contextmenu_registry.ts | 84 +++++++++++---------- core/workspace_svg.ts | 3 +- 4 files changed, 49 insertions(+), 44 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index b2f6c1c9cf8..b8712b01914 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -588,8 +588,7 @@ export class BlockSvg return null; } const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( - ContextMenuRegistry.ScopeType.BLOCK, - {block: this}, + {block: this, focusedNode: this}, e, ); diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index 8a592a78b3c..bcb650b26ff 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -286,8 +286,7 @@ export class RenderedWorkspaceComment /** Show a context menu for this comment. */ showContextMenu(e: Event): void { const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( - ContextMenuRegistry.ScopeType.COMMENT, - {comment: this}, + {comment: this, focusedNode: this}, e, ); diff --git a/core/contextmenu_registry.ts b/core/contextmenu_registry.ts index 06b60801a0a..fc7a94dcb08 100644 --- a/core/contextmenu_registry.ts +++ b/core/contextmenu_registry.ts @@ -13,6 +13,7 @@ import type {BlockSvg} from './block_svg.js'; import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import {Coordinate} from './utils/coordinate.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -71,56 +72,60 @@ export class ContextMenuRegistry { } /** - * Gets the valid context menu options for the given scope type (e.g. block or - * workspace) and scope. Blocks are only shown if the preconditionFn shows + * Gets the valid context menu options for the given scope. + * Options are only included if the preconditionFn shows * they should not be hidden. * - * @param scopeType Type of scope where menu should be shown (e.g. on a block - * or on a workspace) * @param scope Current scope of context menu (i.e., the exact workspace or - * block being clicked on) + * block being clicked on). + * @param menuOpenEvent Event that caused the menu to open. * @returns the list of ContextMenuOptions */ getContextMenuOptions( - scopeType: ScopeType, scope: Scope, menuOpenEvent: Event, ): ContextMenuOption[] { const menuOptions: ContextMenuOption[] = []; for (const item of this.registeredItems.values()) { - if (scopeType === item.scopeType) { - let menuOption: - | ContextMenuRegistry.CoreContextMenuOption - | ContextMenuRegistry.SeparatorContextMenuOption - | ContextMenuRegistry.ActionContextMenuOption; + if (item.scopeType) { + // If the scopeType is present, check to make sure + // that the option is compatible with the current scope + if (item.scopeType === ScopeType.BLOCK && !scope.block) continue; + if (item.scopeType === ScopeType.COMMENT && !scope.comment) continue; + if (item.scopeType === ScopeType.WORKSPACE && !scope.workspace) + continue; + } + let menuOption: + | ContextMenuRegistry.CoreContextMenuOption + | ContextMenuRegistry.SeparatorContextMenuOption + | ContextMenuRegistry.ActionContextMenuOption; + menuOption = { + scope, + weight: item.weight, + }; + + if (item.separator) { menuOption = { - scope, - weight: item.weight, + ...menuOption, + separator: true, }; + } else { + const precondition = item.preconditionFn(scope, menuOpenEvent); + if (precondition === 'hidden') continue; - if (item.separator) { - menuOption = { - ...menuOption, - separator: true, - }; - } else { - const precondition = item.preconditionFn(scope, menuOpenEvent); - if (precondition === 'hidden') continue; - - const displayText = - typeof item.displayText === 'function' - ? item.displayText(scope) - : item.displayText; - menuOption = { - ...menuOption, - text: displayText, - callback: item.callback, - enabled: precondition === 'enabled', - }; - } - - menuOptions.push(menuOption); + const displayText = + typeof item.displayText === 'function' + ? item.displayText(scope) + : item.displayText; + menuOption = { + ...menuOption, + text: displayText, + callback: item.callback, + enabled: precondition === 'enabled', + }; } + + menuOptions.push(menuOption); } menuOptions.sort(function (a, b) { return a.weight - b.weight; @@ -142,20 +147,23 @@ export namespace ContextMenuRegistry { } /** - * The actual workspace/block where the menu is being rendered. This is passed - * to callback and displayText functions that depend on this information. + * The actual workspace/block/focused object where the menu is being + * rendered. This is passed to callback and displayText functions + * that depend on this information. */ export interface Scope { block?: BlockSvg; workspace?: WorkspaceSvg; comment?: RenderedWorkspaceComment; + // TODO(#8839): Remove any once Block, etc. implement IFocusableNode + focusedNode?: IFocusableNode | any; } /** * Fields common to all context menu registry items. */ interface CoreRegistryItem { - scopeType: ScopeType; + scopeType?: ScopeType; weight: number; id: string; } diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index e9414dcdeca..91668b744d4 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -1711,8 +1711,7 @@ export class WorkspaceSvg return; } const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( - ContextMenuRegistry.ScopeType.WORKSPACE, - {workspace: this}, + {workspace: this, focusedNode: this}, e, ); From fd9263ac5150903e5b3ae665e43053e4d5fb771b Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 15 Apr 2025 14:34:38 -0700 Subject: [PATCH 139/222] feat: Allow for HTML elements in dropdown field menus. (#8889) * feat: Allow for HTML elements in dropdown field menus. * refactor: Use dot access. --- core/field_dropdown.ts | 56 ++++++++++++++++++++++-------- tests/mocha/field_dropdown_test.js | 12 +++---- tests/mocha/json_test.js | 12 ------- 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 23a8a3f7da0..81279e2a1f5 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -332,11 +332,11 @@ export class FieldDropdown extends Field { const [label, value] = option; const content = (() => { - if (typeof label === 'object') { + if (isImageProperties(label)) { // Convert ImageProperties to an HTMLImageElement. - const image = new Image(label['width'], label['height']); - image.src = label['src']; - image.alt = label['alt'] || ''; + const image = new Image(label.width, label.height); + image.src = label.src; + image.alt = label.alt; return image; } return label; @@ -497,7 +497,7 @@ export class FieldDropdown extends Field { // Show correct element. const option = this.selectedOption && this.selectedOption[0]; - if (option && typeof option === 'object') { + if (isImageProperties(option)) { this.renderSelectedImage(option); } else { this.renderSelectedText(); @@ -635,8 +635,10 @@ export class FieldDropdown extends Field { return null; } const option = this.selectedOption[0]; - if (typeof option === 'object') { - return option['alt']; + if (isImageProperties(option)) { + return option.alt; + } else if (option instanceof HTMLElement) { + return option.title ?? option.ariaLabel ?? option.innerText; } return option; } @@ -685,10 +687,9 @@ export class FieldDropdown extends Field { hasImages = true; // Copy the image properties so they're not influenced by the original. // NOTE: No need to deep copy since image properties are only 1 level deep. - const imageLabel = - label.alt !== null - ? {...label, alt: parsing.replaceMessageReferences(label.alt)} - : {...label}; + const imageLabel = isImageProperties(label) + ? {...label, alt: parsing.replaceMessageReferences(label.alt)} + : {...label}; return [imageLabel, value]; }); @@ -774,12 +775,13 @@ export class FieldDropdown extends Field { } else if ( option[0] && typeof option[0] !== 'string' && - typeof option[0].src !== 'string' + !isImageProperties(option[0]) && + !(option[0] instanceof HTMLElement) ) { foundError = true; console.error( `Invalid option[${i}]: Each FieldDropdown option must have a string - label or image description. Found ${option[0]} in: ${option}`, + label, image description, or HTML element. Found ${option[0]} in: ${option}`, ); } } @@ -789,6 +791,27 @@ export class FieldDropdown extends Field { } } +/** + * Returns whether or not an object conforms to the ImageProperties interface. + * + * @param obj The object to test. + * @returns True if the object conforms to ImageProperties, otherwise false. + */ +function isImageProperties(obj: any): obj is ImageProperties { + return ( + obj && + typeof obj === 'object' && + 'src' in obj && + typeof obj.src === 'string' && + 'alt' in obj && + typeof obj.alt === 'string' && + 'width' in obj && + typeof obj.width === 'number' && + 'height' in obj && + typeof obj.height === 'number' + ); +} + /** * Definition of a human-readable image dropdown option. */ @@ -803,9 +826,12 @@ export interface ImageProperties { * An individual option in the dropdown menu. Can be either the string literal * `separator` for a menu separator item, or an array for normal action menu * items. In the latter case, the first element is the human-readable value - * (text or image), and the second element is the language-neutral value. + * (text, ImageProperties object, or HTML element), and the second element is + * the language-neutral value. */ -export type MenuOption = [string | ImageProperties, string] | 'separator'; +export type MenuOption = + | [string | ImageProperties | HTMLElement, string] + | 'separator'; /** * A function that generates an array of menu options for FieldDropdown diff --git a/tests/mocha/field_dropdown_test.js b/tests/mocha/field_dropdown_test.js index 61deaf47f39..2ed7098fc9f 100644 --- a/tests/mocha/field_dropdown_test.js +++ b/tests/mocha/field_dropdown_test.js @@ -92,9 +92,9 @@ suite('Dropdown Fields', function () { expectedText: 'a', args: [ [ - [{src: 'scrA', alt: 'a'}, 'A'], - [{src: 'scrB', alt: 'b'}, 'B'], - [{src: 'scrC', alt: 'c'}, 'C'], + [{src: 'scrA', alt: 'a', width: 10, height: 10}, 'A'], + [{src: 'scrB', alt: 'b', width: 10, height: 10}, 'B'], + [{src: 'scrC', alt: 'c', width: 10, height: 10}, 'C'], ], ], }, @@ -121,9 +121,9 @@ suite('Dropdown Fields', function () { args: [ () => { return [ - [{src: 'scrA', alt: 'a'}, 'A'], - [{src: 'scrB', alt: 'b'}, 'B'], - [{src: 'scrC', alt: 'c'}, 'C'], + [{src: 'scrA', alt: 'a', width: 10, height: 10}, 'A'], + [{src: 'scrB', alt: 'b', width: 10, height: 10}, 'B'], + [{src: 'scrC', alt: 'c', width: 10, height: 10}, 'C'], ]; }, ], diff --git a/tests/mocha/json_test.js b/tests/mocha/json_test.js index e9e465c65bc..471d2fb9711 100644 --- a/tests/mocha/json_test.js +++ b/tests/mocha/json_test.js @@ -256,12 +256,6 @@ suite('JSON Block Definitions', function () { 'alt': '%{BKY_ALT_TEXT}', }; const VALUE1 = 'VALUE1'; - const IMAGE2 = { - 'width': 90, - 'height': 123, - 'src': 'http://image2.src', - }; - const VALUE2 = 'VALUE2'; Blockly.defineBlocksWithJsonArray([ { @@ -274,7 +268,6 @@ suite('JSON Block Definitions', function () { 'options': [ [IMAGE0, VALUE0], [IMAGE1, VALUE1], - [IMAGE2, VALUE2], ], }, ], @@ -305,11 +298,6 @@ suite('JSON Block Definitions', function () { assertImageEquals(IMAGE1, image1); assert.equal(image1.alt, IMAGE1_ALT_TEXT); // Via Msg reference assert.equal(VALUE1, options[1][1]); - - const image2 = options[2][0]; - assertImageEquals(IMAGE1, image1); - assert.notExists(image2.alt); // No alt specified. - assert.equal(VALUE2, options[2][1]); }); }); }); From d63a8882c5b29acbc0493095d7147ef802098356 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Wed, 16 Apr 2025 10:48:18 -0700 Subject: [PATCH 140/222] feat: show context menu for connections (#8895) * feat: show context menu for connections * fix: update after rebase --- core/rendered_connection.ts | 40 ++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index c1d97dcddee..168e59744d2 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -18,10 +18,14 @@ import {config} from './config.js'; import {Connection} from './connection.js'; import type {ConnectionDB} from './connection_db.js'; import {ConnectionType} from './connection_type.js'; +import * as ContextMenu from './contextmenu.js'; +import {ContextMenuRegistry} from './contextmenu_registry.js'; import * as eventUtils from './events/utils.js'; +import {IContextMenu} from './interfaces/i_contextmenu.js'; import {hasBubble} from './interfaces/i_has_bubble.js'; import * as internalConstants from './internal_constants.js'; import {Coordinate} from './utils/coordinate.js'; +import * as svgMath from './utils/svg_math.js'; /** Maximum randomness in workspace units for bumping a block. */ const BUMP_RANDOMNESS = 10; @@ -29,7 +33,7 @@ const BUMP_RANDOMNESS = 10; /** * Class for a connection between blocks that may be rendered on screen. */ -export class RenderedConnection extends Connection { +export class RenderedConnection extends Connection implements IContextMenu { // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. sourceBlock_!: BlockSvg; private readonly db: ConnectionDB; @@ -588,6 +592,40 @@ export class RenderedConnection extends Connection { this.sourceBlock_.queueRender(); return this; } + + /** + * Handles showing the context menu when it is opened on a connection. + * Note that typically the context menu can't be opened with the mouse + * on a connection, because you can't select a connection. But keyboard + * users may open the context menu with a keyboard shortcut. + * + * @param e Event that triggered the opening of the context menu. + */ + showContextMenu(e: Event): void { + const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( + {focusedNode: this}, + e, + ); + + if (!menuOptions.length) return; + + const block = this.getSourceBlock(); + const workspace = block.workspace; + + let location; + if (e instanceof PointerEvent) { + location = new Coordinate(e.clientX, e.clientY); + } else { + const connectionWSCoords = new Coordinate(this.x, this.y); + const connectionScreenCoords = svgMath.wsToScreenCoordinates( + workspace, + connectionWSCoords, + ); + location = connectionScreenCoords.translate(block.RTL ? -5 : 5, 5); + } + + ContextMenu.show(e, menuOptions, block.RTL, workspace, location); + } } export namespace RenderedConnection { From 5df6284de220772fd27c7814cd3572d17286b4cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:22:37 -0700 Subject: [PATCH 141/222] chore(deps): bump globals from 15.14.0 to 16.0.0 (#8865) Bumps [globals](https://github.com/sindresorhus/globals) from 15.14.0 to 16.0.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v15.14.0...v16.0.0) --- updated-dependencies: - dependency-name: globals dependency-version: 16.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d1f993c0ea9..ae01e9576d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", - "globals": "^15.12.0", + "globals": "^16.0.0", "google-closure-compiler": "^20240317.0.0", "gulp": "^5.0.0", "gulp-concat": "^2.6.1", @@ -4948,9 +4948,9 @@ } }, "node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 2f8a7e5575a..477ccad6926 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", - "globals": "^15.12.0", + "globals": "^16.0.0", "google-closure-compiler": "^20240317.0.0", "gulp": "^5.0.0", "gulp-concat": "^2.6.1", From 296ad33c21e316920831612e82abc9e96344d457 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 17 Apr 2025 09:48:17 -0700 Subject: [PATCH 142/222] fix: Fix bug that allowed some invisible fields/inputs to be navigated to. (#8899) --- core/keyboard_nav/ast_node.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts index ced10209b95..009e8f5b1e4 100644 --- a/core/keyboard_nav/ast_node.ts +++ b/core/keyboard_nav/ast_node.ts @@ -461,6 +461,8 @@ export class ASTNode { const inputs = block.inputList; for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; + if (!input.isVisible()) continue; + const fieldRow = input.fieldRow; for (let j = 0; j < fieldRow.length; j++) { const field = fieldRow[j]; @@ -468,7 +470,7 @@ export class ASTNode { return ASTNode.createFieldNode(field); } } - if (input.connection && input.isVisible()) { + if (input.connection) { return ASTNode.createInputNode(input); } } From a2d76216b2d2def2a62a2f77981048612502fe54 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Thu, 17 Apr 2025 12:03:35 -0700 Subject: [PATCH 143/222] Feature: Add a dropdown options setter (#8833) * Feature: Add a setOptions method to field_dropdown * add test for changing droopdown options * split out setOptions tests into their own test suite * Add additional tests * auto format files --- core/field_dropdown.ts | 39 ++++++++++++++----------- tests/mocha/field_dropdown_test.js | 46 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 16 deletions(-) diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index bc2d2856f7d..cd8119af824 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -136,26 +136,11 @@ export class FieldDropdown extends Field { // If we pass SKIP_SETUP, don't do *anything* with the menu generator. if (menuGenerator === Field.SKIP_SETUP) return; - if (Array.isArray(menuGenerator)) { - this.validateOptions(menuGenerator); - const trimmed = this.trimOptions(menuGenerator); - this.menuGenerator_ = trimmed.options; - this.prefixField = trimmed.prefix || null; - this.suffixField = trimmed.suffix || null; - } else { - this.menuGenerator_ = menuGenerator; - } - - /** - * The currently selected option. The field is initialized with the - * first option selected. - */ - this.selectedOption = this.getOptions(false)[0]; + this.setOptions(menuGenerator); if (config) { this.configure_(config); } - this.setValue(this.selectedOption[1]); if (validator) { this.setValidator(validator); } @@ -414,6 +399,28 @@ export class FieldDropdown extends Field { return this.generatedOptions; } + /** + * Update the options on this dropdown. This will reset the selected item to + * the first item in the list. + * + * @param menuGenerator The array of options or a generator function. + */ + setOptions(menuGenerator: MenuGenerator) { + if (Array.isArray(menuGenerator)) { + this.validateOptions(menuGenerator); + const trimmed = this.trimOptions(menuGenerator); + this.menuGenerator_ = trimmed.options; + this.prefixField = trimmed.prefix || null; + this.suffixField = trimmed.suffix || null; + } else { + this.menuGenerator_ = menuGenerator; + } + // The currently selected option. The field is initialized with the + // first option selected. + this.selectedOption = this.getOptions(false)[0]; + this.setValue(this.selectedOption[1]); + } + /** * Ensure that the input value is a valid language-neutral option. * diff --git a/tests/mocha/field_dropdown_test.js b/tests/mocha/field_dropdown_test.js index 61deaf47f39..84ce92e2861 100644 --- a/tests/mocha/field_dropdown_test.js +++ b/tests/mocha/field_dropdown_test.js @@ -195,6 +195,52 @@ suite('Dropdown Fields', function () { assertFieldValue(this.field, 'B', 'b'); }); }); + suite('setOptions', function () { + setup(function () { + this.field = new Blockly.FieldDropdown([ + ['a', 'A'], + ['b', 'B'], + ['c', 'C'], + ]); + }); + test('With array updates options', function () { + this.field.setOptions([ + ['d', 'D'], + ['e', 'E'], + ['f', 'F'], + ]); + assertFieldValue(this.field, 'D', 'd'); + }); + test('With generator updates options', function () { + this.field.setOptions(function () { + return [ + ['d', 'D'], + ['e', 'E'], + ['f', 'F'], + ]; + }); + assertFieldValue(this.field, 'D', 'd'); + }); + test('With trimmable options gets trimmed', function () { + this.field.setOptions([ + ['a d b', 'D'], + ['a e b', 'E'], + ['a f b', 'F'], + ]); + assert.deepEqual(this.field.prefixField, 'a'); + assert.deepEqual(this.field.suffixField, 'b'); + assert.deepEqual(this.field.getOptions(), [ + ['d', 'D'], + ['e', 'E'], + ['f', 'F'], + ]); + }); + test('With an empty array of options throws', function () { + assert.throws(function () { + this.field.setOptions([]); + }); + }); + }); suite('Validators', function () { setup(function () { From 5b103e1d7922b2d8dab432b72762b421e34aa3e1 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 17 Apr 2025 15:39:04 -0700 Subject: [PATCH 144/222] fix: Fix bug that caused flyout items under the mouse to be selected without movement. (#8900) --- core/block_flyout_inflater.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/block_flyout_inflater.ts b/core/block_flyout_inflater.ts index b180dbc0c4d..49f65c1f38e 100644 --- a/core/block_flyout_inflater.ts +++ b/core/block_flyout_inflater.ts @@ -222,7 +222,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater { ); blockListeners.push( - browserEvents.bind(block.getSvgRoot(), 'pointerenter', null, () => { + browserEvents.bind(block.getSvgRoot(), 'pointermove', null, () => { if (!this.flyout?.targetWorkspace?.isDragging()) { block.addSelect(); } From 4fb054c48465c1300c320453724535837768d256 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 18 Apr 2025 12:21:52 -0700 Subject: [PATCH 145/222] fix: Recreate the dropdowndiv when clearing it. (#8903) --- core/dropdowndiv.ts | 33 ++++----------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/core/dropdowndiv.ts b/core/dropdowndiv.ts index ef03405f57c..0d259bc53d7 100644 --- a/core/dropdowndiv.ts +++ b/core/dropdowndiv.ts @@ -133,9 +133,6 @@ export function createDom() { // Transition animation for transform: translate() and opacity. div.style.transition = 'transform ' + ANIMATION_TIME + 's, ' + 'opacity ' + ANIMATION_TIME + 's'; - - // Handle focusin/out events to add a visual indicator when - // a child is focused or blurred. } /** @@ -166,8 +163,8 @@ export function getContentDiv(): HTMLDivElement { /** Clear the content of the drop-down. */ export function clearContent() { - content.textContent = ''; - content.style.width = ''; + div.remove(); + createDom(); } /** @@ -338,12 +335,8 @@ export function show( const mainWorkspace = common.getMainWorkspace() as WorkspaceSvg; renderedClassName = mainWorkspace.getRenderer().getClassName(); themeClassName = mainWorkspace.getTheme().getClassName(); - if (renderedClassName) { - dom.addClass(div, renderedClassName); - } - if (themeClassName) { - dom.addClass(div, themeClassName); - } + dom.addClass(div, renderedClassName); + dom.addClass(div, themeClassName); // When we change `translate` multiple times in close succession, // Chrome may choose to wait and apply them all at once. @@ -645,16 +638,6 @@ export function hideWithoutAnimation() { clearTimeout(animateOutTimer); } - // Reset style properties in case this gets called directly - // instead of hide() - see discussion on #2551. - div.style.transform = ''; - div.style.left = ''; - div.style.top = ''; - div.style.opacity = '0'; - div.style.display = 'none'; - div.style.backgroundColor = ''; - div.style.borderColor = ''; - if (onHide) { onHide(); onHide = null; @@ -662,14 +645,6 @@ export function hideWithoutAnimation() { clearContent(); owner = null; - if (renderedClassName) { - dom.removeClass(div, renderedClassName); - renderedClassName = ''; - } - if (themeClassName) { - dom.removeClass(div, themeClassName); - themeClassName = ''; - } (common.getMainWorkspace() as WorkspaceSvg).markFocused(); } From cece3f629623bd6bd08fd2ab1b0e7d6e3a5a1dfe Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 21 Apr 2025 08:54:39 -0700 Subject: [PATCH 146/222] chore: Add messages from the keyboard experiment. (#8904) * chore: Add messages from the keyboard experiment. * fix: Resolve duplicate message ID. * fix: Use placeholders for keyboard shortcuts. --- msg/json/en.json | 33 +++++++++++++- msg/json/qqq.json | 31 +++++++++++++- msg/messages.js | 107 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 3 deletions(-) diff --git a/msg/json/en.json b/msg/json/en.json index 50800bc27e8..a2574b24a64 100644 --- a/msg/json/en.json +++ b/msg/json/en.json @@ -1,7 +1,7 @@ { "@metadata": { "author": "Ellen Spertus ", - "lastupdated": "2024-04-16 23:19:53.668551", + "lastupdated": "2025-04-21 08:25:34.842680", "locale": "en", "messagedocumentation" : "qqq" }, @@ -396,5 +396,34 @@ "WORKSPACE_ARIA_LABEL": "Blockly Workspace", "COLLAPSED_WARNINGS_WARNING": "Collapsed blocks contain warnings.", "DIALOG_OK": "OK", - "DIALOG_CANCEL": "Cancel" + "DIALOG_CANCEL": "Cancel", + "DELETE_SHORTCUT": "Delete block (%1)", + "DELETE_KEY": "Del", + "EDIT_BLOCK_CONTENTS": "Edit Block contents (%1)", + "INSERT_BLOCK": "Insert Block (%1)", + "START_MOVE": "Start move", + "FINISH_MOVE": "Finish move", + "ABORT_MOVE": "Abort move", + "MOVE_LEFT_CONSTRAINED": "Move left, constrained", + "MOVE_RIGHT_CONSTRAINED": "Move right constrained", + "MOVE_UP_CONSTRAINED": "Move up, constrained", + "MOVE_DOWN_CONSTRAINED": "Move down constrained", + "MOVE_LEFT_UNCONSTRAINED": "Move left, unconstrained", + "MOVE_RIGHT_UNCONSTRAINED": "Move right, unconstrained", + "MOVE_UP_UNCONSTRAINED": "Move up unconstrained", + "MOVE_DOWN_UNCONSTRAINED": "Move down, unconstrained", + "MOVE_BLOCK": "Move Block (%1)", + "WINDOWS": "Windows", + "MAC_OS": "macOS", + "CHROME_OS": "ChromeOS", + "LINUX": "Linux", + "UNKNOWN": "Unknown", + "CONTROL_KEY": "Ctrl", + "COMMAND_KEY": "⌘ Command", + "OPTION_KEY": "⌥ Option", + "ALT_KEY": "Alt", + "CUT_SHORTCUT": "Cut (%1)", + "COPY_SHORTCUT": "Copy (%1)", + "PASTE_SHORTCUT": "Paste (%1)", + "HELP_PROMPT": "Press %1 for help on keyboard controls" } diff --git a/msg/json/qqq.json b/msg/json/qqq.json index fcd8897bd04..05fbaafdc9a 100644 --- a/msg/json/qqq.json +++ b/msg/json/qqq.json @@ -402,5 +402,34 @@ "WORKSPACE_ARIA_LABEL": "workspace - This text is read out when a user navigates to the workspace while using a screen reader.", "COLLAPSED_WARNINGS_WARNING": "warning - This appears if the user collapses a block, and blocks inside that block have warnings attached to them. It should inform the user that the block they collapsed contains blocks that have warnings.", "DIALOG_OK": "button label - Pressing this button closes help information.\n{{Identical|OK}}", - "DIALOG_CANCEL": "button label - Pressing this button cancels a proposed action.\n{{Identical|Cancel}}" + "DIALOG_CANCEL": "button label - Pressing this button cancels a proposed action.\n{{Identical|Cancel}}", + "DELETE_SHORTCUT": "menu label - Contextual menu item that deletes the focused block.", + "DELETE_KEY": "menu label - Keyboard shortcut for the Delete key, shown at the end of a menu item that deletes the focused block.", + "EDIT_BLOCK_CONTENTS": "menu label - Contextual menu item that moves the keyboard navigation cursor into a subitem of the focused block.", + "INSERT_BLOCK": "menu label - Contextual menu item that prompts the user to choose a block to insert into the program at the focused location.", + "START_MOVE": "keyboard shortcut label - Contextual menu item that starts a keyboard-driven move of the focused block.", + "FINISH_MOVE": "keyboard shortcut label - Contextual menu item that ends a keyboard-driven move of the focused block.", + "ABORT_MOVE": "keyboard shortcut label - Contextual menu item that ends a keyboard-drive move of the focused block by returning it to its original location.", + "MOVE_LEFT_CONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block to the next valid location to the left.", + "MOVE_RIGHT_CONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block to the next valid location to the right.", + "MOVE_UP_CONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block to the next valid location above it.", + "MOVE_DOWN_CONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block to the next valid location below it.", + "MOVE_LEFT_UNCONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block freely to the left.", + "MOVE_RIGHT_UNCONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block freely to the right.", + "MOVE_UP_UNCONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block freely upwards.", + "MOVE_DOWN_UNCONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block freely downwards.", + "MOVE_BLOCK": "menu label - Contextual menu item that starts a keyboard-driven block move.", + "WINDOWS": "Name of the Microsoft Windows operating system displayed in a list of keyboard shortcuts.", + "MAC_OS": "Name of the Apple macOS operating system displayed in a list of keyboard shortcuts,", + "CHROME_OS": "Name of the Google ChromeOS operating system displayed in a list of keyboard shortcuts.", + "LINUX": "Name of the GNU/Linux operating system displayed in a list of keyboard shortcuts.", + "UNKNOWN": "Placeholder name for an operating system that can't be identified in a list of keyboard shortcuts.", + "CONTROL_KEY": "Representation of the Control key used in keyboard shortcuts.", + "COMMAND_KEY": "Representation of the Mac Command key used in keyboard shortcuts.", + "OPTION_KEY": "Representation of the Mac Option key used in keyboard shortcuts.", + "ALT_KEY": "Representation of the Alt key used in keyboard shortcuts.", + "CUT_SHORTCUT": "menu label - Contextual menu item that cuts the focused item.", + "COPY_SHORTCUT": "menu label - Contextual menu item that copies the focused item.", + "PASTE_SHORTCUT": "menu label - Contextual menu item that pastes the previously copied item.", + "HELP_PROMPT": "Alert message shown to prompt users to review available keyboard shortcuts." } diff --git a/msg/messages.js b/msg/messages.js index 6b9d663a68b..b9a68d957c5 100644 --- a/msg/messages.js +++ b/msg/messages.js @@ -1614,3 +1614,110 @@ Blockly.Msg.DIALOG_OK = 'OK'; /** @type {string} */ /// button label - Pressing this button cancels a proposed action.\n{{Identical|Cancel}} Blockly.Msg.DIALOG_CANCEL = 'Cancel'; + +/** @type {string} */ +/// menu label - Contextual menu item that deletes the focused block. +Blockly.Msg.DELETE_SHORTCUT = 'Delete block (%1)'; +/** @type {string} */ +/// menu label - Keyboard shortcut for the Delete key, shown at the end of a +/// menu item that deletes the focused block. +Blockly.Msg.DELETE_KEY = 'Del'; +/** @type {string} */ +/// menu label - Contextual menu item that moves the keyboard navigation cursor +/// into a subitem of the focused block. +Blockly.Msg.EDIT_BLOCK_CONTENTS = 'Edit Block contents (%1)'; +/** @type {string} */ +/// menu label - Contextual menu item that prompts the user to choose a block to +/// insert into the program at the focused location. +Blockly.Msg.INSERT_BLOCK = 'Insert Block (%1)'; +/** @type {string} */ +/// keyboard shortcut label - Contextual menu item that starts a keyboard-driven +/// move of the focused block. +Blockly.Msg.START_MOVE = 'Start move'; +/** @type {string} */ +/// keyboard shortcut label - Contextual menu item that ends a keyboard-driven +/// move of the focused block. +Blockly.Msg.FINISH_MOVE = 'Finish move'; +/** @type {string} */ +/// keyboard shortcut label - Contextual menu item that ends a keyboard-drive +/// move of the focused block by returning it to its original location. +Blockly.Msg.ABORT_MOVE = 'Abort move'; +/** @type {string} */ +/// keyboard shortcut label - Description of shortcut that moves a block to the +/// next valid location to the left. +Blockly.Msg.MOVE_LEFT_CONSTRAINED = 'Move left, constrained'; +/** @type {string} */ +/// keyboard shortcut label - Description of shortcut that moves a block to the +/// next valid location to the right. +Blockly.Msg.MOVE_RIGHT_CONSTRAINED = 'Move right constrained'; +/** @type {string} */ +/// keyboard shortcut label - Description of shortcut that moves a block to the +/// next valid location above it. +Blockly.Msg.MOVE_UP_CONSTRAINED = 'Move up, constrained'; +/** @type {string} */ +/// keyboard shortcut label - Description of shortcut that moves a block to the +/// next valid location below it. +Blockly.Msg.MOVE_DOWN_CONSTRAINED = 'Move down constrained'; +/** @type {string} */ +/// keyboard shortcut label - Description of shortcut that moves a block freely +/// to the left. +Blockly.Msg.MOVE_LEFT_UNCONSTRAINED = 'Move left, unconstrained'; +/** @type {string} */ +/// keyboard shortcut label - Description of shortcut that moves a block freely +/// to the right. +Blockly.Msg.MOVE_RIGHT_UNCONSTRAINED = 'Move right, unconstrained'; +/** @type {string} */ +/// keyboard shortcut label - Description of shortcut that moves a block freely +/// upwards. +Blockly.Msg.MOVE_UP_UNCONSTRAINED = 'Move up unconstrained'; +/** @type {string} */ +/// keyboard shortcut label - Description of shortcut that moves a block freely +/// downwards. +Blockly.Msg.MOVE_DOWN_UNCONSTRAINED = 'Move down, unconstrained'; +/** @type {string} */ +/// menu label - Contextual menu item that starts a keyboard-driven block move. +Blockly.Msg.MOVE_BLOCK = 'Move Block (%1)'; +/** @type {string} */ +/// Name of the Microsoft Windows operating system displayed in a list of +/// keyboard shortcuts. +Blockly.Msg.WINDOWS = 'Windows'; +/** @type {string} */ +/// Name of the Apple macOS operating system displayed in a list of keyboard +/// shortcuts, +Blockly.Msg.MAC_OS = 'macOS'; +/** @type {string} */ +/// Name of the Google ChromeOS operating system displayed in a list of keyboard +/// shortcuts. +Blockly.Msg.CHROME_OS = 'ChromeOS'; +/** @type {string} */ +/// Name of the GNU/Linux operating system displayed in a list of keyboard +/// shortcuts. +Blockly.Msg.LINUX = 'Linux'; +/** @type {string} */ +/// Placeholder name for an operating system that can't be identified in a list +/// of keyboard shortcuts. +Blockly.Msg.UNKNOWN = 'Unknown'; +/** @type {string} */ +/// Representation of the Control key used in keyboard shortcuts. +Blockly.Msg.CONTROL_KEY = 'Ctrl'; +/** @type {string} */ +/// Representation of the Mac Command key used in keyboard shortcuts. +Blockly.Msg.COMMAND_KEY = '⌘ Command'; +/** @type {string} */ +/// Representation of the Mac Option key used in keyboard shortcuts. +Blockly.Msg.OPTION_KEY = '⌥ Option'; +/** @type {string} */ +/// Representation of the Alt key used in keyboard shortcuts. +Blockly.Msg.ALT_KEY = 'Alt'; +/** @type {string} */ +/// menu label - Contextual menu item that cuts the focused item. +Blockly.Msg.CUT_SHORTCUT = 'Cut (%1)'; +/** @type {string} */ +/// menu label - Contextual menu item that copies the focused item. +Blockly.Msg.COPY_SHORTCUT = 'Copy (%1)'; +/** @type {string} */ +/// menu label - Contextual menu item that pastes the previously copied item. +Blockly.Msg.PASTE_SHORTCUT = 'Paste (%1)'; +/** @type {string} */ +/// Alert message shown to prompt users to review available keyboard shortcuts. +Blockly.Msg.HELP_PROMPT = 'Press %1 for help on keyboard controls'; \ No newline at end of file From bb8df936c2b7cdea7dfa58d52d1a8828caf11a87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 09:04:28 -0700 Subject: [PATCH 147/222] chore(deps): bump typescript from 5.6.3 to 5.8.3 (#8906) Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.6.3 to 5.8.3. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.6.3...v5.8.3) --- updated-dependencies: - dependency-name: typescript dependency-version: 5.8.3 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae01e9576d1..bcd046c05e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8861,10 +8861,11 @@ "dev": true }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From 9d127698d6e30b101a8554e6ff0618341314b1e1 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 21 Apr 2025 10:44:57 -0700 Subject: [PATCH 148/222] fix: Add some missing message strings. (#8908) * fix: Add some missing message strings. * fix: Prefix messages with SHORTCUTS --- msg/json/en.json | 7 +++++-- msg/json/qqq.json | 5 ++++- msg/messages.js | 13 ++++++++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/msg/json/en.json b/msg/json/en.json index a2574b24a64..f28516d35fa 100644 --- a/msg/json/en.json +++ b/msg/json/en.json @@ -1,7 +1,7 @@ { "@metadata": { "author": "Ellen Spertus ", - "lastupdated": "2025-04-21 08:25:34.842680", + "lastupdated": "2025-04-21 10:42:10.549634", "locale": "en", "messagedocumentation" : "qqq" }, @@ -425,5 +425,8 @@ "CUT_SHORTCUT": "Cut (%1)", "COPY_SHORTCUT": "Copy (%1)", "PASTE_SHORTCUT": "Paste (%1)", - "HELP_PROMPT": "Press %1 for help on keyboard controls" + "HELP_PROMPT": "Press %1 for help on keyboard controls", + "SHORTCUTS_GENERAL": "General", + "SHORTCUTS_EDITING": "Editing", + "SHORTCUTS_CODE_NAVIGATION": "Code navigation" } diff --git a/msg/json/qqq.json b/msg/json/qqq.json index 05fbaafdc9a..ffcc393490f 100644 --- a/msg/json/qqq.json +++ b/msg/json/qqq.json @@ -431,5 +431,8 @@ "CUT_SHORTCUT": "menu label - Contextual menu item that cuts the focused item.", "COPY_SHORTCUT": "menu label - Contextual menu item that copies the focused item.", "PASTE_SHORTCUT": "menu label - Contextual menu item that pastes the previously copied item.", - "HELP_PROMPT": "Alert message shown to prompt users to review available keyboard shortcuts." + "HELP_PROMPT": "Alert message shown to prompt users to review available keyboard shortcuts.", + "SHORTCUTS_GENERAL": "shortcut list section header - Label for general purpose keyboard shortcuts.", + "SHORTCUTS_EDITING": "shortcut list section header - Label for keyboard shortcuts related to editing a workspace.", + "SHORTCUTS_CODE_NAVIGATION": "shortcut list section header - Label for keyboard shortcuts related to moving around the workspace." } diff --git a/msg/messages.js b/msg/messages.js index b9a68d957c5..ef332fa3a8e 100644 --- a/msg/messages.js +++ b/msg/messages.js @@ -1720,4 +1720,15 @@ Blockly.Msg.COPY_SHORTCUT = 'Copy (%1)'; Blockly.Msg.PASTE_SHORTCUT = 'Paste (%1)'; /** @type {string} */ /// Alert message shown to prompt users to review available keyboard shortcuts. -Blockly.Msg.HELP_PROMPT = 'Press %1 for help on keyboard controls'; \ No newline at end of file +Blockly.Msg.HELP_PROMPT = 'Press %1 for help on keyboard controls'; +/** @type {string} */ +/// shortcut list section header - Label for general purpose keyboard shortcuts. +Blockly.Msg.SHORTCUTS_GENERAL = 'General'; +/** @type {string} */ +/// shortcut list section header - Label for keyboard shortcuts related to +/// editing a workspace. +Blockly.Msg.SHORTCUTS_EDITING = 'Editing' +/** @type {string} */ +/// shortcut list section header - Label for keyboard shortcuts related to +/// moving around the workspace. +Blockly.Msg.SHORTCUTS_CODE_NAVIGATION = 'Code navigation'; From 0772a298245d76e4291977d352c7ef00745ef725 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 21 Apr 2025 20:37:26 +0000 Subject: [PATCH 149/222] feat!: Introduce new focus tree/node functions. This introduces new callback methods for IFocusableTree and IFocusableNode for providing a basis of synchronizing domain state with focus changes. It also introduces support for implementations of IFocusableTree to better manage initial state cases, especially when a tree is focused using tab navigation. FocusManager has also been updated to ensure functional parity between tab-navigating to a tree and using focusTree() on that tree. This means that tab navigating to a tree will actually restore focus back to that tree's previous focused node rather than the root (unless the root is navigated to from within the tree itself). This is meant to provide better consistency between tab and non-tab keyboard navigation. Note that these changes originally came from #8875 and are required for later PRs that will introduce IFocusableNode and IFocusableTree implementations. --- core/focus_manager.ts | 99 +++++++++++++++++--- core/interfaces/i_focusable_node.ts | 44 ++++++++- core/interfaces/i_focusable_tree.ts | 79 ++++++++++++++++ tests/mocha/focus_manager_test.js | 42 ++++++--- tests/mocha/focusable_tree_traverser_test.js | 12 +++ 5 files changed, 247 insertions(+), 29 deletions(-) diff --git a/core/focus_manager.ts b/core/focus_manager.ts index c1fc295b991..26cc1a0c511 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -60,6 +60,7 @@ export class FocusManager { registeredTrees: Array = []; private currentlyHoldsEphemeralFocus: boolean = false; + private lockFocusStateChanges: boolean = false; constructor( addGlobalEventListener: (type: string, listener: EventListener) => void, @@ -89,7 +90,16 @@ export class FocusManager { } if (newNode) { - this.focusNode(newNode); + const newTree = newNode.getFocusableTree(); + const oldTree = this.focusedNode?.getFocusableTree(); + if (newNode === newTree.getRootFocusableNode() && newTree !== oldTree) { + // If the root of the tree is the one taking focus (such as due to + // being tabbed), try to focus the whole tree explicitly to ensure the + // correct node re-receives focus. + this.focusTree(newTree); + } else { + this.focusNode(newNode); + } } else { this.defocusCurrentFocusedNode(); } @@ -108,6 +118,7 @@ export class FocusManager { * certain whether the tree has been registered. */ registerTree(tree: IFocusableTree): void { + this.ensureManagerIsUnlocked(); if (this.isRegistered(tree)) { throw Error(`Attempted to re-register already registered tree: ${tree}.`); } @@ -133,6 +144,7 @@ export class FocusManager { * this manager. */ unregisterTree(tree: IFocusableTree): void { + this.ensureManagerIsUnlocked(); if (!this.isRegistered(tree)) { throw Error(`Attempted to unregister not registered tree: ${tree}.`); } @@ -192,11 +204,14 @@ export class FocusManager { * focus. */ focusTree(focusableTree: IFocusableTree): void { + this.ensureManagerIsUnlocked(); if (!this.isRegistered(focusableTree)) { throw Error(`Attempted to focus unregistered tree: ${focusableTree}.`); } const currNode = FocusableTreeTraverser.findFocusedNode(focusableTree); - this.focusNode(currNode ?? focusableTree.getRootFocusableNode()); + const nodeToRestore = focusableTree.getRestoredFocusableNode(currNode); + const rootFallback = focusableTree.getRootFocusableNode(); + this.focusNode(nodeToRestore ?? currNode ?? rootFallback); } /** @@ -205,18 +220,37 @@ export class FocusManager { * Any previously focused node will be updated to be passively highlighted (if * it's in a different focusable tree) or blurred (if it's in the same one). * - * @param focusableNode The node that should receive active - * focus. + * @param focusableNode The node that should receive active focus. */ focusNode(focusableNode: IFocusableNode): void { + this.ensureManagerIsUnlocked(); + if (this.focusedNode == focusableNode) return; // State is unchanged. + const nextTree = focusableNode.getFocusableTree(); if (!this.isRegistered(nextTree)) { throw Error(`Attempted to focus unregistered node: ${focusableNode}.`); } + + // Safety check for ensuring focusNode() doesn't get called for a node that + // isn't actually hooked up to its parent tree correctly (since this can + // cause weird inconsistencies). + const matchedNode = FocusableTreeTraverser.findFocusableNodeFor( + focusableNode.getFocusableElement(), + nextTree, + ); + if (matchedNode !== focusableNode) { + throw Error( + `Attempting to focus node which isn't recognized by its parent tree: ` + + `${focusableNode}.`, + ); + } + const prevNode = this.focusedNode; - if (prevNode && prevNode.getFocusableTree() !== nextTree) { - this.setNodeToPassive(prevNode); + const prevTree = prevNode?.getFocusableTree(); + if (prevNode && prevTree !== nextTree) { + this.passivelyFocusNode(prevNode, nextTree); } + // If there's a focused node in the new node's tree, ensure it's reset. const prevNodeNextTree = FocusableTreeTraverser.findFocusedNode(nextTree); const nextTreeRoot = nextTree.getRootFocusableNode(); @@ -229,9 +263,10 @@ export class FocusManager { if (nextTreeRoot !== focusableNode) { this.removeHighlight(nextTreeRoot); } + if (!this.currentlyHoldsEphemeralFocus) { // Only change the actively focused node if ephemeral state isn't held. - this.setNodeToActive(focusableNode); + this.activelyFocusNode(focusableNode, prevTree ?? null); } this.focusedNode = focusableNode; } @@ -257,6 +292,7 @@ export class FocusManager { takeEphemeralFocus( focusableElement: HTMLElement | SVGElement, ): ReturnEphemeralFocus { + this.ensureManagerIsUnlocked(); if (this.currentlyHoldsEphemeralFocus) { throw Error( `Attempted to take ephemeral focus when it's already held, ` + @@ -266,7 +302,7 @@ export class FocusManager { this.currentlyHoldsEphemeralFocus = true; if (this.focusedNode) { - this.setNodeToPassive(this.focusedNode); + this.passivelyFocusNode(this.focusedNode, null); } focusableElement.focus(); @@ -282,29 +318,66 @@ export class FocusManager { this.currentlyHoldsEphemeralFocus = false; if (this.focusedNode) { - this.setNodeToActive(this.focusedNode); + this.activelyFocusNode(this.focusedNode, null); } }; } + private ensureManagerIsUnlocked(): void { + if (this.lockFocusStateChanges) { + throw Error( + 'FocusManager state changes cannot happen in a tree/node focus/blur ' + + 'callback.', + ); + } + } + private defocusCurrentFocusedNode(): void { // The current node will likely be defocused while ephemeral focus is held, // but internal manager state shouldn't change since the node should be // restored upon exiting ephemeral focus mode. if (this.focusedNode && !this.currentlyHoldsEphemeralFocus) { - this.setNodeToPassive(this.focusedNode); + this.passivelyFocusNode(this.focusedNode, null); this.focusedNode = null; } } - private setNodeToActive(node: IFocusableNode): void { + private activelyFocusNode( + node: IFocusableNode, + prevTree: IFocusableTree | null, + ): void { + // Note that order matters here. Focus callbacks are allowed to change + // element visibility which can influence focusability, including for a + // node's focusable element (which *is* allowed to be invisible until the + // node needs to be focused). + this.lockFocusStateChanges = true; + node.getFocusableTree().onTreeFocus(node, prevTree); + node.onNodeFocus(); + this.lockFocusStateChanges = false; + + this.setNodeToVisualActiveFocus(node); + node.getFocusableElement().focus(); + } + + private passivelyFocusNode( + node: IFocusableNode, + nextTree: IFocusableTree | null, + ): void { + this.lockFocusStateChanges = true; + node.getFocusableTree().onTreeBlur(nextTree); + node.onNodeBlur(); + this.lockFocusStateChanges = false; + + this.setNodeToVisualPassiveFocus(node); + } + + private setNodeToVisualActiveFocus(node: IFocusableNode): void { const element = node.getFocusableElement(); dom.addClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); dom.removeClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); - element.focus(); } - private setNodeToPassive(node: IFocusableNode): void { + private setNodeToVisualPassiveFocus(node: IFocusableNode): void { const element = node.getFocusableElement(); dom.removeClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); dom.addClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts index 14100d44c7f..44bdf8be0db 100644 --- a/core/interfaces/i_focusable_node.ts +++ b/core/interfaces/i_focusable_node.ts @@ -25,8 +25,14 @@ export interface IFocusableNode { * and a tab index must be present in order for the element to be focusable in * the DOM). * - * It's expected the return element will not change for the lifetime of the - * node. + * The returned element must be visible if the node is ever focused via + * FocusManager.focusNode() or FocusManager.focusTree(). It's allowed for an + * element to be hidden until onNodeFocus() is called, or become hidden with a + * call to onNodeBlur(). + * + * It's expected the actual returned element will not change for the lifetime + * of the node (that is, its properties can change but a new element should + * never be returned.) */ getFocusableElement(): HTMLElement | SVGElement; @@ -36,4 +42,38 @@ export interface IFocusableNode { * belongs. */ getFocusableTree(): IFocusableTree; + + /** + * Called when this node receives active focus. + * + * Note that it's fine for implementations to change visibility modifiers, but + * they should avoid the following: + * - Creating or removing DOM elements (including via the renderer or drawer). + * - Affecting focus via DOM focus() calls or the FocusManager. + */ + onNodeFocus(): void; + + /** + * Called when this node loses active focus. It may still have passive focus. + * + * This has the same implementation restrictions as onNodeFocus(). + */ + onNodeBlur(): void; +} + +/** + * Determines whether the provided object fulfills the contract of + * IFocusableNode. + * + * @param object The object to test. + * @returns Whether the provided object can be used as an IFocusableNode. + */ +export function isFocusableNode(object: any | null): object is IFocusableNode { + return ( + object && + 'getFocusableElement' in object && + 'getFocusableTree' in object && + 'onNodeFocus' in object && + 'onNodeBlur' in object + ); } diff --git a/core/interfaces/i_focusable_tree.ts b/core/interfaces/i_focusable_tree.ts index bc0c38849c8..364f4cad630 100644 --- a/core/interfaces/i_focusable_tree.ts +++ b/core/interfaces/i_focusable_tree.ts @@ -37,6 +37,34 @@ export interface IFocusableTree { */ getRootFocusableNode(): IFocusableNode; + /** + * Returns the IFocusableNode of this tree that should receive active focus + * when the tree itself has focused returned to it. + * + * There are some very important notes to consider about a tree's focus + * lifecycle when implementing a version of this method that doesn't return + * null: + * 1. A null previousNode does not guarantee first-time focus state as nodes + * can be deleted. + * 2. This method is only used when the tree itself is focused, either through + * tab navigation or via FocusManager.focusTree(). In many cases, the + * previously focused node will be directly focused instead which will + * bypass this method. + * 3. The default behavior (i.e. returning null here) involves either + * restoring the previous node (previousNode) or focusing the tree's root. + * + * This method is largely intended to provide tree implementations with the + * means of specifying a better default node than their root. + * + * @param previousNode The node that previously held passive focus for this + * tree, or null if the tree hasn't yet been focused. + * @returns The IFocusableNode that should now receive focus, or null if + * default behavior should be used, instead. + */ + getRestoredFocusableNode( + previousNode: IFocusableNode | null, + ): IFocusableNode | null; + /** * Returns all directly nested trees under this tree. * @@ -58,4 +86,55 @@ export interface IFocusableTree { * @param id The ID of the node's focusable HTMLElement or SVGElement. */ lookUpFocusableNode(id: string): IFocusableNode | null; + + /** + * Called when a node of this tree has received active focus. + * + * Note that a null previousTree does not necessarily indicate that this is + * the first time Blockly is receiving focus. In fact, few assumptions can be + * made about previous focus state as a previous null tree simply indicates + * that Blockly did not hold active focus prior to this tree becoming focused + * (which can happen due to focus exiting the Blockly injection div, or for + * other cases like ephemeral focus). + * + * See IFocusableNode.onNodeFocus() as implementations have the same + * restrictions as with that method. + * + * @param node The node receiving active focus. + * @param previousTree The previous tree that held active focus, or null if + * none. + */ + onTreeFocus(node: IFocusableNode, previousTree: IFocusableTree | null): void; + + /** + * Called when the previously actively focused node of this tree is now + * passively focused and there is no other active node of this tree taking its + * place. + * + * This has the same implementation restrictions and considerations as + * onTreeFocus(). + * + * @param nextTree The next tree receiving active focus, or null if none (such + * as in the case that Blockly is entirely losing DOM focus). + */ + onTreeBlur(nextTree: IFocusableTree | null): void; +} + +/** + * Determines whether the provided object fulfills the contract of + * IFocusableTree. + * + * @param object The object to test. + * @returns Whether the provided object can be used as an IFocusableTree. + */ +export function isFocusableTree(object: any | null): object is IFocusableTree { + return ( + object && + 'getRootFocusableNode' in object && + 'getRestoredFocusableNode' in object && + 'getNestedTrees' in object && + 'lookUpFocusableNode' in object && + 'onTreeFocus' in object && + 'onTreeBlur' in object + ); } diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js index 4a3f6b3ad1f..70ef210c587 100644 --- a/tests/mocha/focus_manager_test.js +++ b/tests/mocha/focus_manager_test.js @@ -27,6 +27,10 @@ class FocusableNodeImpl { getFocusableTree() { return this.tree; } + + onNodeFocus() {} + + onNodeBlur() {} } class FocusableTreeImpl { @@ -46,6 +50,10 @@ class FocusableTreeImpl { return this.rootNode; } + getRestoredFocusableNode() { + return null; + } + getNestedTrees() { return this.nestedTrees; } @@ -53,6 +61,10 @@ class FocusableTreeImpl { lookUpFocusableNode(id) { return this.idToNodeMap[id]; } + + onTreeFocus() {} + + onTreeBlur() {} } suite('FocusManager', function () { @@ -2067,7 +2079,7 @@ suite('FocusManager', function () { ); }); - test('registered tree focus()ed other tree node passively focused tree root now has active property', function () { + test('registered tree focus()ed other tree node passively focused tree node now has active property', function () { this.focusManager.registerTree(this.testFocusableTree1); this.focusManager.registerTree(this.testFocusableTree2); document.getElementById('testFocusableTree1.node1').focus(); @@ -2075,26 +2087,27 @@ suite('FocusManager', function () { document.getElementById('testFocusableTree1').focus(); - // This differs from the behavior of focusTree() since directly focusing a tree's root will - // coerce it to now have focus. + // Directly refocusing a tree's root should have functional parity with focusTree(). That + // means the tree's previous node should now have active focus again and its root should + // have no focus indication. const rootElem = this.testFocusableTree1 .getRootFocusableNode() .getFocusableElement(); const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); assert.includesClass( - rootElem.classList, + nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); assert.notIncludesClass( - rootElem.classList, + nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); assert.notIncludesClass( - nodeElem.classList, + rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); assert.notIncludesClass( - nodeElem.classList, + rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); @@ -3879,7 +3892,7 @@ suite('FocusManager', function () { ); }); - test('registered tree focus()ed other tree node passively focused tree root now has active property', function () { + test('registered tree focus()ed other tree node passively focused tree node now has active property', function () { this.focusManager.registerTree(this.testFocusableGroup1); this.focusManager.registerTree(this.testFocusableGroup2); document.getElementById('testFocusableGroup1.node1').focus(); @@ -3887,26 +3900,27 @@ suite('FocusManager', function () { document.getElementById('testFocusableGroup1').focus(); - // This differs from the behavior of focusTree() since directly focusing a tree's root will - // coerce it to now have focus. + // Directly refocusing a tree's root should have functional parity with focusTree(). That + // means the tree's previous node should now have active focus again and its root should + // have no focus indication. const rootElem = this.testFocusableGroup1 .getRootFocusableNode() .getFocusableElement(); const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); assert.includesClass( - rootElem.classList, + nodeElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); assert.notIncludesClass( - rootElem.classList, + nodeElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); assert.notIncludesClass( - nodeElem.classList, + rootElem.classList, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); assert.notIncludesClass( - nodeElem.classList, + rootElem.classList, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, ); }); diff --git a/tests/mocha/focusable_tree_traverser_test.js b/tests/mocha/focusable_tree_traverser_test.js index b6674573ecd..d2467b6e95c 100644 --- a/tests/mocha/focusable_tree_traverser_test.js +++ b/tests/mocha/focusable_tree_traverser_test.js @@ -25,6 +25,10 @@ class FocusableNodeImpl { getFocusableTree() { return this.tree; } + + onNodeFocus() {} + + onNodeBlur() {} } class FocusableTreeImpl { @@ -44,6 +48,10 @@ class FocusableTreeImpl { return this.rootNode; } + getRestoredFocusableNode() { + return null; + } + getNestedTrees() { return this.nestedTrees; } @@ -51,6 +59,10 @@ class FocusableTreeImpl { lookUpFocusableNode(id) { return this.idToNodeMap[id]; } + + onTreeFocus() {} + + onTreeBlur() {} } suite('FocusableTreeTraverser', function () { From 404c20eeaf5b0927ae65112256f5a040054bbcc6 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 21 Apr 2025 20:55:02 +0000 Subject: [PATCH 150/222] chore: Remove unused isFocusable*() functions. These were needed in previous versions of plugin changes, but aren't anymore. --- core/interfaces/i_focusable_node.ts | 17 ----------------- core/interfaces/i_focusable_tree.ts | 19 ------------------- 2 files changed, 36 deletions(-) diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts index 44bdf8be0db..0e81cd8dcb9 100644 --- a/core/interfaces/i_focusable_node.ts +++ b/core/interfaces/i_focusable_node.ts @@ -60,20 +60,3 @@ export interface IFocusableNode { */ onNodeBlur(): void; } - -/** - * Determines whether the provided object fulfills the contract of - * IFocusableNode. - * - * @param object The object to test. - * @returns Whether the provided object can be used as an IFocusableNode. - */ -export function isFocusableNode(object: any | null): object is IFocusableNode { - return ( - object && - 'getFocusableElement' in object && - 'getFocusableTree' in object && - 'onNodeFocus' in object && - 'onNodeBlur' in object - ); -} diff --git a/core/interfaces/i_focusable_tree.ts b/core/interfaces/i_focusable_tree.ts index 364f4cad630..9d1e68559c7 100644 --- a/core/interfaces/i_focusable_tree.ts +++ b/core/interfaces/i_focusable_tree.ts @@ -119,22 +119,3 @@ export interface IFocusableTree { */ onTreeBlur(nextTree: IFocusableTree | null): void; } - -/** - * Determines whether the provided object fulfills the contract of - * IFocusableTree. - * - * @param object The object to test. - * @returns Whether the provided object can be used as an IFocusableTree. - */ -export function isFocusableTree(object: any | null): object is IFocusableTree { - return ( - object && - 'getRootFocusableNode' in object && - 'getRestoredFocusableNode' in object && - 'getNestedTrees' in object && - 'lookUpFocusableNode' in object && - 'onTreeFocus' in object && - 'onTreeBlur' in object - ); -} From c91fed3fdb775250b5c30d07c934be65e5db5fae Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 21 Apr 2025 21:00:27 +0000 Subject: [PATCH 151/222] chore: equality + doc cleanups --- core/focus_manager.ts | 2 +- core/interfaces/i_focusable_node.ts | 2 +- core/interfaces/i_focusable_tree.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/focus_manager.ts b/core/focus_manager.ts index 26cc1a0c511..230bdf030e1 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -224,7 +224,7 @@ export class FocusManager { */ focusNode(focusableNode: IFocusableNode): void { this.ensureManagerIsUnlocked(); - if (this.focusedNode == focusableNode) return; // State is unchanged. + if (this.focusedNode === focusableNode) return; // State is unchanged. const nextTree = focusableNode.getFocusableTree(); if (!this.isRegistered(nextTree)) { diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts index 0e81cd8dcb9..06d43acea1f 100644 --- a/core/interfaces/i_focusable_node.ts +++ b/core/interfaces/i_focusable_node.ts @@ -32,7 +32,7 @@ export interface IFocusableNode { * * It's expected the actual returned element will not change for the lifetime * of the node (that is, its properties can change but a new element should - * never be returned.) + * never be returned). */ getFocusableElement(): HTMLElement | SVGElement; diff --git a/core/interfaces/i_focusable_tree.ts b/core/interfaces/i_focusable_tree.ts index 9d1e68559c7..699328ef8d6 100644 --- a/core/interfaces/i_focusable_tree.ts +++ b/core/interfaces/i_focusable_tree.ts @@ -39,7 +39,7 @@ export interface IFocusableTree { /** * Returns the IFocusableNode of this tree that should receive active focus - * when the tree itself has focused returned to it. + * when the tree itself has focus returned to it. * * There are some very important notes to consider about a tree's focus * lifecycle when implementing a version of this method that doesn't return From 4e8bb9850f35c0fbdceddd1ac3526487156534e3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 21 Apr 2025 21:09:26 +0000 Subject: [PATCH 152/222] Revert "chore: Remove unused isFocusable*() functions." This reverts commit 404c20eeaf5b0927ae65112256f5a040054bbcc6. --- core/interfaces/i_focusable_node.ts | 17 +++++++++++++++++ core/interfaces/i_focusable_tree.ts | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts index 06d43acea1f..53a432d30f4 100644 --- a/core/interfaces/i_focusable_node.ts +++ b/core/interfaces/i_focusable_node.ts @@ -60,3 +60,20 @@ export interface IFocusableNode { */ onNodeBlur(): void; } + +/** + * Determines whether the provided object fulfills the contract of + * IFocusableNode. + * + * @param object The object to test. + * @returns Whether the provided object can be used as an IFocusableNode. + */ +export function isFocusableNode(object: any | null): object is IFocusableNode { + return ( + object && + 'getFocusableElement' in object && + 'getFocusableTree' in object && + 'onNodeFocus' in object && + 'onNodeBlur' in object + ); +} diff --git a/core/interfaces/i_focusable_tree.ts b/core/interfaces/i_focusable_tree.ts index 699328ef8d6..69afa24ffdf 100644 --- a/core/interfaces/i_focusable_tree.ts +++ b/core/interfaces/i_focusable_tree.ts @@ -119,3 +119,22 @@ export interface IFocusableTree { */ onTreeBlur(nextTree: IFocusableTree | null): void; } + +/** + * Determines whether the provided object fulfills the contract of + * IFocusableTree. + * + * @param object The object to test. + * @returns Whether the provided object can be used as an IFocusableTree. + */ +export function isFocusableTree(object: any | null): object is IFocusableTree { + return ( + object && + 'getRootFocusableNode' in object && + 'getRestoredFocusableNode' in object && + 'getNestedTrees' in object && + 'lookUpFocusableNode' in object && + 'onTreeFocus' in object && + 'onTreeBlur' in object + ); +} From b3bce678f84080131f9e10f812b2916bd491e772 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Mon, 21 Apr 2025 14:11:49 -0700 Subject: [PATCH 153/222] Fix: Remove the collapsed block warning when expanding a block (#8854) * Fix: Remove the collapsed block warning when expanding a block * Add tests and make warnings appear as soon as a block collapses * Combine if checks to simplify code --- core/block_svg.ts | 23 ++++++++++++++++ tests/mocha/block_test.js | 56 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/core/block_svg.ts b/core/block_svg.ts index 10fa995ffda..8e016efb32f 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -507,6 +507,21 @@ export class BlockSvg this.updateCollapsed(); } + /** + * Traverses child blocks to see if any of them have a warning. + * + * @returns true if any child has a warning, false otherwise. + */ + private childHasWarning(): boolean { + const children = this.getChildren(false); + for (const child of children) { + if (child.getIcon(WarningIcon.TYPE) || child.childHasWarning()) { + return true; + } + } + return false; + } + /** * Makes sure that when the block is collapsed, it is rendered correctly * for that state. @@ -529,9 +544,17 @@ export class BlockSvg if (!collapsed) { this.updateDisabled(); this.removeInput(collapsedInputName); + this.setWarningText(null, BlockSvg.COLLAPSED_WARNING_ID); return; } + if (this.childHasWarning()) { + this.setWarningText( + Msg['COLLAPSED_WARNINGS_WARNING'], + BlockSvg.COLLAPSED_WARNING_ID, + ); + } + const text = this.toString(internalConstants.COLLAPSE_CHARS); const field = this.getField(collapsedFieldName); if (field) { diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index a489fb3e3c5..ce9036cc4d9 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -1840,6 +1840,62 @@ suite('Blocks', function () { }); }); + suite('Warning icons and collapsing', function () { + setup(function () { + this.workspace = Blockly.inject('blocklyDiv'); + this.parentBlock = Blockly.serialization.blocks.append( + { + 'type': 'statement_block', + 'inputs': { + 'STATEMENT': { + 'block': { + 'type': 'statement_block', + }, + }, + }, + }, + this.workspace, + ); + this.parentBlock.initSvg(); + this.parentBlock.render(); + + this.childBlock = this.parentBlock.getInputTargetBlock('STATEMENT'); + this.childBlock.initSvg(); + this.childBlock.render(); + }); + + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + + test('Adding a warning to a child block does not affect the parent', function () { + const text = 'Warning Text'; + this.childBlock.setWarningText(text); + const icon = this.parentBlock.getIcon(Blockly.icons.WarningIcon.TYPE); + assert.isUndefined( + icon, + "Setting a child block's warning should not add a warning to the parent", + ); + }); + + test('Warnings are added and removed when collapsing a stack with warnings', function () { + const text = 'Warning Text'; + + this.childBlock.setWarningText(text); + + this.parentBlock.setCollapsed(true); + let icon = this.parentBlock.getIcon(Blockly.icons.WarningIcon.TYPE); + assert.exists(icon?.getText(), 'Expected warning icon text to be set'); + + this.parentBlock.setCollapsed(false); + icon = this.parentBlock.getIcon(Blockly.icons.WarningIcon.TYPE); + assert.isUndefined( + icon, + 'Warning should be removed from parent after expanding', + ); + }); + }); + suite('Bubbles and collapsing', function () { setup(function () { this.workspace = Blockly.inject('blocklyDiv'); From c6e58c4f92dece0b3da241e0cc31ea09f922b7ce Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 21 Apr 2025 15:32:45 -0700 Subject: [PATCH 154/222] feat: Add support for displaying toast-style notifications. (#8896) * feat: Allow resetting alert/prompt/confirm to defaults. * chore: Add unit tests for Blockly.dialog. * fix: Removed TEST_ONLY hack from Blockly.dialog. * feat: Add a default toast notification implementation. * feat: Add support for toasts to Blockly.dialog. * chore: Add tests for default toast implementation. * chore: Fix docstring. * refactor: Use default arguments for dialog functions. * refactor: Add 'close' to the list of messages. * chore: Add new message in several other places. * chore: clarify docstrings. * feat: Make toast assertiveness configurable. --- core/blockly.ts | 2 + core/dialog.ts | 93 +++++++---- core/toast.ts | 219 ++++++++++++++++++++++++++ core/utils/aria.ts | 11 ++ msg/json/en.json | 1 + msg/json/qqq.json | 1 + msg/messages.js | 3 + tests/mocha/contextmenu_items_test.js | 19 ++- tests/mocha/dialog_test.js | 168 ++++++++++++++++++++ tests/mocha/index.html | 2 + tests/mocha/test_helpers/workspace.js | 18 +-- tests/mocha/toast_test.js | 129 +++++++++++++++ 12 files changed, 620 insertions(+), 46 deletions(-) create mode 100644 core/toast.ts create mode 100644 tests/mocha/dialog_test.js create mode 100644 tests/mocha/toast_test.js diff --git a/core/blockly.ts b/core/blockly.ts index 46ea1fcaf43..c38a1d48e4b 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -432,6 +432,8 @@ Names.prototype.populateProcedures = function ( }; // clang-format on +export * from './toast.js'; + // Re-export submodules that no longer declareLegacyNamespace. export { ASTNode, diff --git a/core/dialog.ts b/core/dialog.ts index 7e21129855c..374961323da 100644 --- a/core/dialog.ts +++ b/core/dialog.ts @@ -6,24 +6,29 @@ // Former goog.module ID: Blockly.dialog -let alertImplementation = function ( - message: string, - opt_callback?: () => void, -) { +import type {ToastOptions} from './toast.js'; +import {Toast} from './toast.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +const defaultAlert = function (message: string, opt_callback?: () => void) { window.alert(message); if (opt_callback) { opt_callback(); } }; -let confirmImplementation = function ( +let alertImplementation = defaultAlert; + +const defaultConfirm = function ( message: string, callback: (result: boolean) => void, ) { callback(window.confirm(message)); }; -let promptImplementation = function ( +let confirmImplementation = defaultConfirm; + +const defaultPrompt = function ( message: string, defaultValue: string, callback: (result: string | null) => void, @@ -31,6 +36,11 @@ let promptImplementation = function ( callback(window.prompt(message, defaultValue)); }; +let promptImplementation = defaultPrompt; + +const defaultToast = Toast.show.bind(Toast); +let toastImplementation = defaultToast; + /** * Wrapper to window.alert() that app developers may override via setAlert to * provide alternatives to the modal browser window. @@ -45,10 +55,16 @@ export function alert(message: string, opt_callback?: () => void) { /** * Sets the function to be run when Blockly.dialog.alert() is called. * - * @param alertFunction The function to be run. + * @param alertFunction The function to be run, or undefined to restore the + * default implementation. * @see Blockly.dialog.alert */ -export function setAlert(alertFunction: (p1: string, p2?: () => void) => void) { +export function setAlert( + alertFunction: ( + message: string, + callback?: () => void, + ) => void = defaultAlert, +) { alertImplementation = alertFunction; } @@ -59,25 +75,22 @@ export function setAlert(alertFunction: (p1: string, p2?: () => void) => void) { * @param message The message to display to the user. * @param callback The callback for handling user response. */ -export function confirm(message: string, callback: (p1: boolean) => void) { - TEST_ONLY.confirmInternal(message, callback); -} - -/** - * Private version of confirm for stubbing in tests. - */ -function confirmInternal(message: string, callback: (p1: boolean) => void) { +export function confirm(message: string, callback: (result: boolean) => void) { confirmImplementation(message, callback); } /** * Sets the function to be run when Blockly.dialog.confirm() is called. * - * @param confirmFunction The function to be run. + * @param confirmFunction The function to be run, or undefined to restore the + * default implementation. * @see Blockly.dialog.confirm */ export function setConfirm( - confirmFunction: (p1: string, p2: (p1: boolean) => void) => void, + confirmFunction: ( + message: string, + callback: (result: boolean) => void, + ) => void = defaultConfirm, ) { confirmImplementation = confirmFunction; } @@ -95,7 +108,7 @@ export function setConfirm( export function prompt( message: string, defaultValue: string, - callback: (p1: string | null) => void, + callback: (result: string | null) => void, ) { promptImplementation(message, defaultValue, callback); } @@ -103,19 +116,45 @@ export function prompt( /** * Sets the function to be run when Blockly.dialog.prompt() is called. * - * @param promptFunction The function to be run. + * @param promptFunction The function to be run, or undefined to restore the + * default implementation. * @see Blockly.dialog.prompt */ export function setPrompt( promptFunction: ( - p1: string, - p2: string, - p3: (p1: string | null) => void, - ) => void, + message: string, + defaultValue: string, + callback: (result: string | null) => void, + ) => void = defaultPrompt, ) { promptImplementation = promptFunction; } -export const TEST_ONLY = { - confirmInternal, -}; +/** + * Displays a temporary notification atop the workspace. Blockly provides a + * default toast implementation, but developers may provide their own via + * setToast. For simple appearance customization, CSS should be sufficient. + * + * @param workspace The workspace to display the toast notification atop. + * @param options Configuration options for the notification, including its + * message and duration. + */ +export function toast(workspace: WorkspaceSvg, options: ToastOptions) { + toastImplementation(workspace, options); +} + +/** + * Sets the function to be run when Blockly.dialog.toast() is called. + * + * @param toastFunction The function to be run, or undefined to restore the + * default implementation. + * @see Blockly.dialog.toast + */ +export function setToast( + toastFunction: ( + workspace: WorkspaceSvg, + options: ToastOptions, + ) => void = defaultToast, +) { + toastImplementation = toastFunction; +} diff --git a/core/toast.ts b/core/toast.ts new file mode 100644 index 00000000000..72559279f57 --- /dev/null +++ b/core/toast.ts @@ -0,0 +1,219 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Css from './css.js'; +import {Msg} from './msg.js'; +import * as aria from './utils/aria.js'; +import * as dom from './utils/dom.js'; +import {Svg} from './utils/svg.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +const CLASS_NAME = 'blocklyToast'; +const MESSAGE_CLASS_NAME = 'blocklyToastMessage'; +const CLOSE_BUTTON_CLASS_NAME = 'blocklyToastCloseButton'; + +/** + * Display/configuration options for a toast notification. + */ +export interface ToastOptions { + /** + * Toast ID. If set along with `oncePerSession`, will cause subsequent toasts + * with this ID to not be shown. + */ + id?: string; + + /** + * Flag to show the toast once per session only. + * Subsequent calls are ignored. + */ + oncePerSession?: boolean; + + /** + * Text of the message to display on the toast. + */ + message: string; + + /** + * Duration in seconds before the toast is removed. Defaults to 5. + */ + duration?: number; + + /** + * How prominently/interrupting the readout of the toast should be for + * screenreaders. Corresponds to aria-live and defaults to polite. + */ + assertiveness?: Toast.Assertiveness; +} + +/** + * Class that allows for showing and dismissing temporary notifications. + */ +export class Toast { + /** IDs of toasts that have previously been shown. */ + private static shownIds = new Set(); + + /** + * Shows a toast notification. + * + * @param workspace The workspace to show the toast on. + * @param options Configuration options for the toast message, duration, etc. + */ + static show(workspace: WorkspaceSvg, options: ToastOptions) { + if (options.oncePerSession && options.id) { + if (this.shownIds.has(options.id)) return; + this.shownIds.add(options.id); + } + + // Clear any existing toasts. + this.hide(workspace); + + const toast = this.createDom(workspace, options); + + // Animate the toast into view. + requestAnimationFrame(() => { + toast.style.bottom = '2rem'; + }); + } + + /** + * Creates the DOM representation of a toast. + * + * @param workspace The workspace to inject the toast notification onto. + * @param options Configuration options for the toast. + * @returns The root DOM element of the toast. + */ + protected static createDom(workspace: WorkspaceSvg, options: ToastOptions) { + const { + message, + duration = 5, + assertiveness = Toast.Assertiveness.POLITE, + } = options; + + const toast = document.createElement('div'); + workspace.getInjectionDiv().appendChild(toast); + toast.dataset.toastId = options.id; + toast.className = CLASS_NAME; + aria.setRole(toast, aria.Role.STATUS); + aria.setState(toast, aria.State.LIVE, assertiveness); + + const messageElement = toast.appendChild(document.createElement('div')); + messageElement.className = MESSAGE_CLASS_NAME; + messageElement.innerText = message; + const closeButton = toast.appendChild(document.createElement('button')); + closeButton.className = CLOSE_BUTTON_CLASS_NAME; + aria.setState(closeButton, aria.State.LABEL, Msg['CLOSE']); + const closeIcon = dom.createSvgElement( + Svg.SVG, + { + width: 24, + height: 24, + viewBox: '0 0 24 24', + fill: 'none', + }, + closeButton, + ); + aria.setState(closeIcon, aria.State.HIDDEN, true); + dom.createSvgElement( + Svg.RECT, + { + x: 19.7782, + y: 2.80762, + width: 2, + height: 24, + transform: 'rotate(45, 19.7782, 2.80762)', + fill: 'black', + }, + closeIcon, + ); + dom.createSvgElement( + Svg.RECT, + { + x: 2.80762, + y: 4.22183, + width: 2, + height: 24, + transform: 'rotate(-45, 2.80762, 4.22183)', + fill: 'black', + }, + closeIcon, + ); + closeButton.addEventListener('click', () => { + toast.remove(); + workspace.markFocused(); + }); + + let timeout: ReturnType; + const setToastTimeout = () => { + timeout = setTimeout(() => toast.remove(), duration * 1000); + }; + const clearToastTimeout = () => clearTimeout(timeout); + toast.addEventListener('focusin', clearToastTimeout); + toast.addEventListener('focusout', setToastTimeout); + toast.addEventListener('mouseenter', clearToastTimeout); + toast.addEventListener('mousemove', clearToastTimeout); + toast.addEventListener('mouseleave', setToastTimeout); + setToastTimeout(); + + return toast; + } + + /** + * Dismiss a toast, e.g. in response to a user action. + * + * @param workspace The workspace to dismiss a toast in. + * @param id The toast ID, or undefined to clear any toast. + */ + static hide(workspace: WorkspaceSvg, id?: string) { + const toast = workspace.getInjectionDiv().querySelector(`.${CLASS_NAME}`); + if (toast instanceof HTMLElement && (!id || id === toast.dataset.toastId)) { + toast.remove(); + } + } +} + +/** + * Options for how aggressively toasts should be read out by screenreaders. + * Values correspond to those for aria-live. + */ +export namespace Toast { + export enum Assertiveness { + ASSERTIVE = 'assertive', + POLITE = 'polite', + } +} + +Css.register(` +.${CLASS_NAME} { + font-size: 1.2rem; + position: absolute; + bottom: -10rem; + right: 2rem; + padding: 1rem; + color: black; + background-color: white; + border: 2px solid black; + border-radius: 0.4rem; + z-index: 999; + display: flex; + align-items: center; + gap: 0.8rem; + line-height: 1.5; + transition: bottom 0.3s ease-out; +} + +.${CLASS_NAME} .${MESSAGE_CLASS_NAME} { + maxWidth: 18rem; +} + +.${CLASS_NAME} .${CLOSE_BUTTON_CLASS_NAME} { + margin: 0; + padding: 0.2rem; + background-color: transparent; + color: black; + border: none; + cursor: pointer; +} +`); diff --git a/core/utils/aria.ts b/core/utils/aria.ts index 8089298e4ec..d997b8d0af0 100644 --- a/core/utils/aria.ts +++ b/core/utils/aria.ts @@ -51,6 +51,9 @@ export enum Role { // ARIA role for a visual separator in e.g. a menu. SEPARATOR = 'separator', + + // ARIA role for a live region providing information. + STATUS = 'status', } /** @@ -110,6 +113,14 @@ export enum State { // ARIA property for slider minimum value. Value: number. VALUEMIN = 'valuemin', + + // ARIA property for live region chattiness. + // Value: one of {polite, assertive, off}. + LIVE = 'live', + + // ARIA property for removing elements from the accessibility tree. + // Value: one of {true, false, undefined}. + HIDDEN = 'hidden', } /** diff --git a/msg/json/en.json b/msg/json/en.json index f28516d35fa..e7c468d288a 100644 --- a/msg/json/en.json +++ b/msg/json/en.json @@ -18,6 +18,7 @@ "DELETE_X_BLOCKS": "Delete %1 Blocks", "DELETE_ALL_BLOCKS": "Delete all %1 blocks?", "CLEAN_UP": "Clean up Blocks", + "CLOSE": "Close", "COLLAPSE_BLOCK": "Collapse Block", "COLLAPSE_ALL": "Collapse Blocks", "EXPAND_BLOCK": "Expand Block", diff --git a/msg/json/qqq.json b/msg/json/qqq.json index ffcc393490f..c4de18656a0 100644 --- a/msg/json/qqq.json +++ b/msg/json/qqq.json @@ -24,6 +24,7 @@ "DELETE_X_BLOCKS": "context menu - Permanently delete the %1 selected blocks.\n\nParameters:\n* %1 - an integer greater than 1.", "DELETE_ALL_BLOCKS": "confirmation prompt - Question the user if they really wanted to permanently delete all %1 blocks.\n\nParameters:\n* %1 - an integer greater than 1.", "CLEAN_UP": "context menu - Reposition all the blocks so that they form a neat line.", + "CLOSE": "toast notification - Accessibility label for close button.", "COLLAPSE_BLOCK": "context menu - Make the appearance of the selected block smaller by hiding some information about it.", "COLLAPSE_ALL": "context menu - Make the appearance of all blocks smaller by hiding some information about it. Use the same terminology as in the previous message.", "EXPAND_BLOCK": "context menu - Restore the appearance of the selected block by showing information about it that was hidden (collapsed) earlier.", diff --git a/msg/messages.js b/msg/messages.js index ef332fa3a8e..d0c3e17688a 100644 --- a/msg/messages.js +++ b/msg/messages.js @@ -103,6 +103,9 @@ Blockly.Msg.DELETE_ALL_BLOCKS = 'Delete all %1 blocks?'; /// context menu - Reposition all the blocks so that they form a neat line. Blockly.Msg.CLEAN_UP = 'Clean up Blocks'; /** @type {string} */ +/// toast notification - Accessibility label for close button. +Blockly.Msg.CLOSE = 'Close'; +/** @type {string} */ /// context menu - Make the appearance of the selected block smaller by hiding some information about it. Blockly.Msg.COLLAPSE_BLOCK = 'Collapse Block'; /** @type {string} */ diff --git a/tests/mocha/contextmenu_items_test.js b/tests/mocha/contextmenu_items_test.js index a9e2bb3de62..d9044ec7e28 100644 --- a/tests/mocha/contextmenu_items_test.js +++ b/tests/mocha/contextmenu_items_test.js @@ -318,9 +318,7 @@ suite('Context Menu Items', function () { test('Deletes all blocks after confirming', function () { // Mocks the confirmation dialog and calls the callback with 'true' simulating ok. - const confirmStub = sinon - .stub(Blockly.dialog.TEST_ONLY, 'confirmInternal') - .callsArgWith(1, true); + const confirmStub = sinon.stub(window, 'confirm').returns(true); this.workspace.newBlock('text'); this.workspace.newBlock('text'); @@ -328,13 +326,13 @@ suite('Context Menu Items', function () { this.clock.runAll(); sinon.assert.calledOnce(confirmStub); assert.equal(this.workspace.getTopBlocks(false).length, 0); + + confirmStub.restore(); }); test('Does not delete blocks if not confirmed', function () { // Mocks the confirmation dialog and calls the callback with 'false' simulating cancel. - const confirmStub = sinon - .stub(Blockly.dialog.TEST_ONLY, 'confirmInternal') - .callsArgWith(1, false); + const confirmStub = sinon.stub(window, 'confirm').returns(false); this.workspace.newBlock('text'); this.workspace.newBlock('text'); @@ -342,19 +340,20 @@ suite('Context Menu Items', function () { this.clock.runAll(); sinon.assert.calledOnce(confirmStub); assert.equal(this.workspace.getTopBlocks(false).length, 2); + + confirmStub.restore(); }); test('No dialog for single block', function () { - const confirmStub = sinon.stub( - Blockly.dialog.TEST_ONLY, - 'confirmInternal', - ); + const confirmStub = sinon.stub(window, 'confirm'); this.workspace.newBlock('text'); this.deleteOption.callback(this.scope); this.clock.runAll(); sinon.assert.notCalled(confirmStub); assert.equal(this.workspace.getTopBlocks(false).length, 0); + + confirmStub.restore(); }); test('Has correct label for multiple blocks', function () { diff --git a/tests/mocha/dialog_test.js b/tests/mocha/dialog_test.js new file mode 100644 index 00000000000..f250ff0f8aa --- /dev/null +++ b/tests/mocha/dialog_test.js @@ -0,0 +1,168 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {assert} from '../../node_modules/chai/chai.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Dialog utilities', function () { + setup(function () { + sharedTestSetup.call(this); + this.workspace = Blockly.inject('blocklyDiv', {}); + }); + + teardown(function () { + sharedTestTeardown.call(this); + Blockly.dialog.setAlert(); + Blockly.dialog.setPrompt(); + Blockly.dialog.setConfirm(); + Blockly.dialog.setToast(); + }); + + test('use the browser alert by default', function () { + const alert = sinon.stub(window, 'alert'); + Blockly.dialog.alert('test'); + assert.isTrue(alert.calledWith('test')); + alert.restore(); + }); + + test('support setting a custom alert handler', function () { + const alert = sinon.spy(); + Blockly.dialog.setAlert(alert); + const callback = () => {}; + const message = 'test'; + Blockly.dialog.alert(message, callback); + assert.isTrue(alert.calledWith('test', callback)); + }); + + test('do not call the browser alert if a custom alert handler is set', function () { + const browserAlert = sinon.stub(window, 'alert'); + + const alert = sinon.spy(); + Blockly.dialog.setAlert(alert); + Blockly.dialog.alert(test); + assert.isFalse(browserAlert.called); + + browserAlert.restore(); + }); + + test('use the browser confirm by default', function () { + const confirm = sinon.stub(window, 'confirm'); + const callback = () => {}; + const message = 'test'; + Blockly.dialog.confirm(message, callback); + assert.isTrue(confirm.calledWith(message)); + confirm.restore(); + }); + + test('support setting a custom confirm handler', function () { + const confirm = sinon.spy(); + Blockly.dialog.setConfirm(confirm); + const callback = () => {}; + const message = 'test'; + Blockly.dialog.confirm(message, callback); + assert.isTrue(confirm.calledWith('test', callback)); + }); + + test('do not call the browser confirm if a custom confirm handler is set', function () { + const browserConfirm = sinon.stub(window, 'confirm'); + + const confirm = sinon.spy(); + Blockly.dialog.setConfirm(confirm); + const callback = () => {}; + const message = 'test'; + Blockly.dialog.confirm(message, callback); + assert.isFalse(browserConfirm.called); + + browserConfirm.restore(); + }); + + test('invokes the provided callback with the confirmation response', function () { + const confirm = sinon.stub(window, 'confirm').returns(true); + const callback = sinon.spy(); + const message = 'test'; + Blockly.dialog.confirm(message, callback); + assert.isTrue(callback.calledWith(true)); + confirm.restore(); + }); + + test('use the browser prompt by default', function () { + const prompt = sinon.stub(window, 'prompt'); + const callback = () => {}; + const message = 'test'; + const defaultValue = 'default'; + Blockly.dialog.prompt(message, defaultValue, callback); + assert.isTrue(prompt.calledWith(message, defaultValue)); + prompt.restore(); + }); + + test('support setting a custom prompt handler', function () { + const prompt = sinon.spy(); + Blockly.dialog.setPrompt(prompt); + const callback = () => {}; + const message = 'test'; + const defaultValue = 'default'; + Blockly.dialog.prompt(message, defaultValue, callback); + assert.isTrue(prompt.calledWith('test', defaultValue, callback)); + }); + + test('do not call the browser prompt if a custom prompt handler is set', function () { + const browserPrompt = sinon.stub(window, 'prompt'); + + const prompt = sinon.spy(); + Blockly.dialog.setPrompt(prompt); + const callback = () => {}; + const message = 'test'; + const defaultValue = 'default'; + Blockly.dialog.prompt(message, defaultValue, callback); + assert.isFalse(browserPrompt.called); + + browserPrompt.restore(); + }); + + test('invokes the provided callback with the prompt response', function () { + const prompt = sinon.stub(window, 'prompt').returns('something'); + const callback = sinon.spy(); + const message = 'test'; + const defaultValue = 'default'; + Blockly.dialog.prompt(message, defaultValue, callback); + assert.isTrue(callback.calledWith('something')); + prompt.restore(); + }); + + test('use the built-in toast by default', function () { + const message = 'test toast'; + Blockly.dialog.toast(this.workspace, {message}); + const toast = this.workspace + .getInjectionDiv() + .querySelector('.blocklyToast'); + assert.isNotNull(toast); + assert.equal(toast.textContent, message); + }); + + test('support setting a custom toast handler', function () { + const toast = sinon.spy(); + Blockly.dialog.setToast(toast); + const message = 'test toast'; + const options = {message}; + Blockly.dialog.toast(this.workspace, options); + assert.isTrue(toast.calledWith(this.workspace, options)); + }); + + test('do not use the built-in toast if a custom toast handler is set', function () { + const builtInToast = sinon.stub(Blockly.Toast, 'show'); + + const toast = sinon.spy(); + Blockly.dialog.setToast(toast); + const message = 'test toast'; + Blockly.dialog.toast(this.workspace, {message}); + assert.isFalse(builtInToast.called); + + builtInToast.restore(); + }); +}); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 690b75a7759..1c9f1fbbc6a 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -192,6 +192,7 @@ import './contextmenu_items_test.js'; import './contextmenu_test.js'; import './cursor_test.js'; + import './dialog_test.js'; import './dropdowndiv_test.js'; import './event_test.js'; import './event_block_change_test.js'; @@ -260,6 +261,7 @@ import './shortcut_registry_test.js'; import './touch_test.js'; import './theme_test.js'; + import './toast_test.js'; import './toolbox_test.js'; import './tooltip_test.js'; import './trashcan_test.js'; diff --git a/tests/mocha/test_helpers/workspace.js b/tests/mocha/test_helpers/workspace.js index 40b2574fca1..917ce6f629e 100644 --- a/tests/mocha/test_helpers/workspace.js +++ b/tests/mocha/test_helpers/workspace.js @@ -100,9 +100,7 @@ export function testAWorkspace() { test('deleteVariableById(id2) one usage', function () { // Deleting variable one usage should not trigger confirm dialog. - const stub = sinon - .stub(Blockly.dialog.TEST_ONLY, 'confirmInternal') - .callsArgWith(1, true); + const stub = sinon.stub(window, 'confirm').returns(true); this.workspace.deleteVariableById('id2'); sinon.assert.notCalled(stub); @@ -110,13 +108,13 @@ export function testAWorkspace() { assert.isNull(variable); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertBlockVarModelName(this.workspace, 0, 'name1'); + + stub.restore(); }); test('deleteVariableById(id1) multiple usages confirm', function () { // Deleting variable with multiple usages triggers confirm dialog. - const stub = sinon - .stub(Blockly.dialog.TEST_ONLY, 'confirmInternal') - .callsArgWith(1, true); + const stub = sinon.stub(window, 'confirm').returns(true); this.workspace.deleteVariableById('id1'); sinon.assert.calledOnce(stub); @@ -124,13 +122,13 @@ export function testAWorkspace() { assert.isNull(variable); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); assertBlockVarModelName(this.workspace, 0, 'name2'); + + stub.restore(); }); test('deleteVariableById(id1) multiple usages cancel', function () { // Deleting variable with multiple usages triggers confirm dialog. - const stub = sinon - .stub(Blockly.dialog.TEST_ONLY, 'confirmInternal') - .callsArgWith(1, false); + const stub = sinon.stub(window, 'confirm').returns(false); this.workspace.deleteVariableById('id1'); sinon.assert.calledOnce(stub); @@ -139,6 +137,8 @@ export function testAWorkspace() { assertBlockVarModelName(this.workspace, 0, 'name1'); assertBlockVarModelName(this.workspace, 1, 'name1'); assertBlockVarModelName(this.workspace, 2, 'name2'); + + stub.restore(); }); }); diff --git a/tests/mocha/toast_test.js b/tests/mocha/toast_test.js new file mode 100644 index 00000000000..45e02ad5de8 --- /dev/null +++ b/tests/mocha/toast_test.js @@ -0,0 +1,129 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {assert} from '../../node_modules/chai/chai.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Toasts', function () { + setup(function () { + sharedTestSetup.call(this); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.toastIsVisible = (message) => { + const toast = this.workspace + .getInjectionDiv() + .querySelector('.blocklyToast'); + return !!(toast && toast.textContent === message); + }; + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + test('can be shown', function () { + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, {message}); + assert.isTrue(this.toastIsVisible(message)); + }); + + test('can be shown only once per session', function () { + const options = { + message: 'texas toast', + id: 'test', + oncePerSession: true, + }; + Blockly.Toast.show(this.workspace, options); + assert.isTrue(this.toastIsVisible(options.message)); + Blockly.Toast.hide(this.workspace); + Blockly.Toast.show(this.workspace, options); + assert.isFalse(this.toastIsVisible(options.message)); + }); + + test('oncePerSession is ignored when false', function () { + const options = { + message: 'texas toast', + id: 'some id', + oncePerSession: true, + }; + Blockly.Toast.show(this.workspace, options); + assert.isTrue(this.toastIsVisible(options.message)); + Blockly.Toast.hide(this.workspace); + options.oncePerSession = false; + Blockly.Toast.show(this.workspace, options); + assert.isTrue(this.toastIsVisible(options.message)); + }); + + test('can be hidden', function () { + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, {message}); + assert.isTrue(this.toastIsVisible(message)); + Blockly.Toast.hide(this.workspace); + assert.isFalse(this.toastIsVisible(message)); + }); + + test('can be hidden by ID', function () { + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, {message, id: 'test'}); + assert.isTrue(this.toastIsVisible(message)); + Blockly.Toast.hide(this.workspace, 'test'); + assert.isFalse(this.toastIsVisible(message)); + }); + + test('hide does not hide toasts with different ID', function () { + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, {message, id: 'test'}); + assert.isTrue(this.toastIsVisible(message)); + Blockly.Toast.hide(this.workspace, 'test2'); + assert.isTrue(this.toastIsVisible(message)); + }); + + test('are shown for the designated duration', function () { + const clock = sinon.useFakeTimers(); + + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, {message, duration: 3}); + for (let i = 0; i < 3; i++) { + assert.isTrue(this.toastIsVisible(message)); + clock.tick(1000); + } + assert.isFalse(this.toastIsVisible(message)); + + clock.restore(); + }); + + test('default to polite assertiveness', function () { + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, {message, id: 'test'}); + const toast = this.workspace + .getInjectionDiv() + .querySelector('.blocklyToast'); + + assert.equal( + toast.getAttribute('aria-live'), + Blockly.Toast.Assertiveness.POLITE, + ); + }); + + test('respects assertiveness option', function () { + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, { + message, + id: 'test', + assertiveness: Blockly.Toast.Assertiveness.ASSERTIVE, + }); + const toast = this.workspace + .getInjectionDiv() + .querySelector('.blocklyToast'); + + assert.equal( + toast.getAttribute('aria-live'), + Blockly.Toast.Assertiveness.ASSERTIVE, + ); + }); +}); From 7c0c8536e6b51af9088bc928ac26e9ae73209c29 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 22 Apr 2025 00:49:52 +0000 Subject: [PATCH 155/222] fix: Fix broken FocusManager tree unregistration. --- core/focus_manager.ts | 2 +- tests/mocha/focus_manager_test.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/focus_manager.ts b/core/focus_manager.ts index 230bdf030e1..c6d40fa0d0f 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -148,7 +148,7 @@ export class FocusManager { if (!this.isRegistered(tree)) { throw Error(`Attempted to unregister not registered tree: ${tree}.`); } - const treeIndex = this.registeredTrees.findIndex((tree) => tree === tree); + const treeIndex = this.registeredTrees.findIndex((reg) => reg === tree); this.registeredTrees.splice(treeIndex, 1); const focusedNode = FocusableTreeTraverser.findFocusedNode(tree); diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js index 70ef210c587..69ecfe722a5 100644 --- a/tests/mocha/focus_manager_test.js +++ b/tests/mocha/focus_manager_test.js @@ -305,6 +305,18 @@ suite('FocusManager', function () { assert.isTrue(isRegistered); }); + + test('for unregistered tree with other registered tree returns false', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isFalse(isRegistered); + }); }); suite('getFocusedTree()', function () { From 0a95939a8ed4df52496a298d2db9be013eed0f7d Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 23 Apr 2025 10:46:26 -0700 Subject: [PATCH 156/222] chore: Try another workflow permission. (#8811) * chore: Try another workflow permission. * chore: Explicitly specify the GitHub token. * chore: Try with contents: write. * chore: Try write-all at the top level. * chore: try regular pull_request. * chore: Fix assign reviewers action configuration. --- .github/workflows/assign_reviewers.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/assign_reviewers.yml b/.github/workflows/assign_reviewers.yml index 924bf5423a2..850c7c805ff 100644 --- a/.github/workflows/assign_reviewers.yml +++ b/.github/workflows/assign_reviewers.yml @@ -16,8 +16,7 @@ jobs: requested-reviewer: runs-on: ubuntu-latest permissions: - contents: read - issues: write + pull-requests: write steps: - name: Assign requested reviewer uses: actions/github-script@v7 From 2564239d23437a133b33209c03bcad7e57848365 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 23 Apr 2025 21:22:37 +0000 Subject: [PATCH 157/222] chore: Add private method documentation. Addresses a review comment. --- core/focus_manager.ts | 53 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/core/focus_manager.ts b/core/focus_manager.ts index c6d40fa0d0f..83755067c4c 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -323,6 +323,13 @@ export class FocusManager { }; } + /** + * Ensures that the manager is currently allowing operations that change its + * internal focus state (such as via focusNode()). + * + * If the manager is currently not allowing state changes, an exception is + * thrown. + */ private ensureManagerIsUnlocked(): void { if (this.lockFocusStateChanges) { throw Error( @@ -332,6 +339,10 @@ export class FocusManager { } } + /** + * Defocuses the current actively focused node tracked by the manager, if + * there is one iff the manager isn't in an ephemeral focus state. + */ private defocusCurrentFocusedNode(): void { // The current node will likely be defocused while ephemeral focus is held, // but internal manager state shouldn't change since the node should be @@ -342,6 +353,18 @@ export class FocusManager { } } + /** + * Marks the specified node as actively focused, also calling related lifecycle + * callback methods for both the node and its parent tree. This ensures that + * the node is properly styled to indicate its active focus. + * + * This does not change the manager's currently tracked node, nor does it + * change any other nodes. + * + * @param node The node to be actively focused. + * @param prevTree The tree of the previously actively focused node, or null + * if there wasn't a previously actively focused node. + */ private activelyFocusNode( node: IFocusableNode, prevTree: IFocusableTree | null, @@ -359,6 +382,18 @@ export class FocusManager { node.getFocusableElement().focus(); } + /** + * Marks the specified node as passively focused, also calling related + * lifecycle callback methods for both the node and its parent tree. This + * ensures that the node is properly styled to indicate its passive focus. + * + * This does not change the manager's currently tracked node, nor does it + * change any other nodes. + * + * @param node The node to be passively focused. + * @param nextTree The tree of the node receiving active focus, or null if no + * node will be actively focused. + */ private passivelyFocusNode( node: IFocusableNode, nextTree: IFocusableTree | null, @@ -371,18 +406,36 @@ export class FocusManager { this.setNodeToVisualPassiveFocus(node); } + /** + * Updates the node's styling to indicate that it should have an active focus + * indicator. + * + * @param node The node to be styled for active focus. + */ private setNodeToVisualActiveFocus(node: IFocusableNode): void { const element = node.getFocusableElement(); dom.addClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); dom.removeClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); } + /** + * Updates the node's styling to indicate that it should have a passive focus + * indicator. + * + * @param node The node to be styled for passive focus. + */ private setNodeToVisualPassiveFocus(node: IFocusableNode): void { const element = node.getFocusableElement(); dom.removeClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); dom.addClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); } + /** + * Removes any active/passive indicators for the specified node. + * + * @param node The node which should have neither passive nor active focus + * indication. + */ private removeHighlight(node: IFocusableNode): void { const element = node.getFocusableElement(); dom.removeClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); From 096e7711cb85a719dcdf310dbe06fb3aa8265854 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 23 Apr 2025 21:24:05 +0000 Subject: [PATCH 158/222] chore: clean-up documentation comment. --- core/focus_manager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/focus_manager.ts b/core/focus_manager.ts index 83755067c4c..88eef46b530 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -340,8 +340,8 @@ export class FocusManager { } /** - * Defocuses the current actively focused node tracked by the manager, if - * there is one iff the manager isn't in an ephemeral focus state. + * Defocuses the current actively focused node tracked by the manager, iff + * there's a node being tracked and the manager doesn't have ephemeral focus. */ private defocusCurrentFocusedNode(): void { // The current node will likely be defocused while ephemeral focus is held, From d7680cf32ec0fff5b1fabf8d786f996e9e454ff2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 24 Apr 2025 14:48:16 -0700 Subject: [PATCH 159/222] feat: Make WorkspaceSvg and BlockSvg focusable (#8916) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #8913 Fixes #8914 Fixes part of #8771 ### Proposed Changes This updates `WorkspaceSvg` and `BlockSvg` to be focusable, that is, it makes the workspace a `IFocusableTree` and blocks `IFocusableNode`s. Some important details: - While this introduces focusable tree support for `Workspace` it doesn't include two other components that are obviously needed by the keyboard navigation plugin's playground: fields and connections. These will be introduced in subsequent PRs. - Blocks are set up to automatically synchronize their selection state with their focus state. This will eventually help to replace `LineCursor`'s responsibility for managing selection state itself. - The tabindex property for the workspace and its ARIA label have been moved down to the `.blocklyWorkspace` element itself rather than its wrapper. This helps address some tab stop issues that are already addressed in the plugin (via monkey patches), but also to ensure that the workspace's main SVG group interacts correctly with `FocusManager`. - `WorkspaceSvg` is being initially set up to default to its first top block when being focused for the first time. This is to match parity with the keyboard navigation plugin, however the latter also has functionality for defaulting to a position when no blocks are present. It's not clear how to actually support this under the new focus-based system (without adding an ephemeral element on which to focus), or if it's even necessary (since the workspace root can hold focus). ### Reason for Changes This is part of an ongoing effort to ensure key components of Blockly are focusable so that they can be keyboard-navigable (with other needed changes yet both in Core Blockly and the keyboard navigation plugin). ### Test Coverage No new tests have been added. It's certainly possible to add unit tests for the focusable configurations being introduced in this PR, but it may not be highly beneficial. It's largely assumed that the individual implementations should work due to a highly tested FocusManager, and it may be the case that the interactions of the components working together is far more important to verify (that is, the end user flows). The latter is planned to be tackled as part of #8915. ### Documentation No documentation changes should be needed here. ### Additional Information This includes changes that have been pulled from #8875. --- core/block_svg.ts | 28 +++++++++- core/inject.ts | 5 -- core/renderers/common/path_object.ts | 2 +- core/workspace_svg.ts | 77 +++++++++++++++++++++++++++- 4 files changed, 103 insertions(+), 9 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index b8712b01914..8bc0b7af3f6 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -44,6 +44,8 @@ import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {ICopyable} from './interfaces/i_copyable.js'; import {IDeletable} from './interfaces/i_deletable.js'; import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {IIcon} from './interfaces/i_icon.js'; import * as internalConstants from './internal_constants.js'; import {MarkerManager} from './marker_manager.js'; @@ -76,7 +78,8 @@ export class BlockSvg IContextMenu, ICopyable, IDraggable, - IDeletable + IDeletable, + IFocusableNode { /** * Constant for identifying rows that are to be rendered inline. @@ -210,6 +213,7 @@ export class BlockSvg // Expose this block's ID on its top-level SVG group. this.svgGroup.setAttribute('data-id', this.id); + svgPath.id = this.id; this.doInit_(); } @@ -1819,4 +1823,26 @@ export class BlockSvg ); } } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + return this.pathObject.svgPath; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.workspace; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void { + common.setSelected(this); + } + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void { + if (common.getSelected() === this) { + common.setSelected(null); + } + } } diff --git a/core/inject.ts b/core/inject.ts index de78fbfae75..34d9c1795f8 100644 --- a/core/inject.ts +++ b/core/inject.ts @@ -13,13 +13,11 @@ import * as common from './common.js'; import * as Css from './css.js'; import * as dropDownDiv from './dropdowndiv.js'; import {Grid} from './grid.js'; -import {Msg} from './msg.js'; import {Options} from './options.js'; import {ScrollbarPair} from './scrollbar_pair.js'; import {ShortcutRegistry} from './shortcut_registry.js'; import * as Tooltip from './tooltip.js'; import * as Touch from './touch.js'; -import * as aria from './utils/aria.js'; import * as dom from './utils/dom.js'; import {Svg} from './utils/svg.js'; import * as WidgetDiv from './widgetdiv.js'; @@ -56,8 +54,6 @@ export function inject( if (opt_options?.rtl) { dom.addClass(subContainer, 'blocklyRTL'); } - subContainer.tabIndex = 0; - aria.setState(subContainer, aria.State.LABEL, Msg['WORKSPACE_ARIA_LABEL']); containerElement!.appendChild(subContainer); const svg = createDom(subContainer, options); @@ -126,7 +122,6 @@ function createDom(container: HTMLElement, options: Options): SVGElement { 'xmlns:xlink': dom.XLINK_NS, 'version': '1.1', 'class': 'blocklySvg', - 'tabindex': '0', }, container, ); diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index 077f80bb741..72cf2a594ce 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -62,7 +62,7 @@ export class PathObject implements IPathObject { /** The primary path of the block. */ this.svgPath = dom.createSvgElement( Svg.PATH, - {'class': 'blocklyPath'}, + {'class': 'blocklyPath', 'tabindex': '-1'}, this.svgRoot, ); diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 91668b744d4..b1c96373771 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -37,6 +37,7 @@ import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import {Flyout} from './flyout_base.js'; import type {FlyoutButton} from './flyout_button.js'; +import {getFocusManager} from './focus_manager.js'; import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; @@ -44,6 +45,8 @@ import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {IDragTarget} from './interfaces/i_drag_target.js'; import type {IFlyout} from './interfaces/i_flyout.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; import type {IToolbox} from './interfaces/i_toolbox.js'; import type { @@ -54,6 +57,7 @@ import type {LineCursor} from './keyboard_nav/line_cursor.js'; import type {Marker} from './keyboard_nav/marker.js'; import {LayerManager} from './layer_manager.js'; import {MarkerManager} from './marker_manager.js'; +import {Msg} from './msg.js'; import {Options} from './options.js'; import * as Procedures from './procedures.js'; import * as registry from './registry.js'; @@ -66,6 +70,7 @@ import {Classic} from './theme/classic.js'; import {ThemeManager} from './theme_manager.js'; import * as Tooltip from './tooltip.js'; import type {Trashcan} from './trashcan.js'; +import * as aria from './utils/aria.js'; import * as arrayUtils from './utils/array.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; @@ -93,7 +98,7 @@ const ZOOM_TO_FIT_MARGIN = 20; */ export class WorkspaceSvg extends Workspace - implements IASTNodeLocationSvg, IContextMenu + implements IASTNodeLocationSvg, IContextMenu, IFocusableNode, IFocusableTree { /** * A wrapper function called when a resize event occurs. @@ -764,7 +769,19 @@ export class WorkspaceSvg * * */ - this.svgGroup_ = dom.createSvgElement(Svg.G, {'class': 'blocklyWorkspace'}); + this.svgGroup_ = dom.createSvgElement(Svg.G, { + 'class': 'blocklyWorkspace', + // Only the top-level workspace should be tabbable. + 'tabindex': injectionDiv ? '0' : '-1', + 'id': this.id, + }); + if (injectionDiv) { + aria.setState( + this.svgGroup_, + aria.State.LABEL, + Msg['WORKSPACE_ARIA_LABEL'], + ); + } // Note that a alone does not receive mouse events--it must have a // valid target inside it. If no background class is specified, as in the @@ -840,6 +857,9 @@ export class WorkspaceSvg this.getTheme(), isParentWorkspace ? this.getInjectionDiv() : undefined, ); + + getFocusManager().registerTree(this); + return this.svgGroup_; } @@ -924,6 +944,10 @@ export class WorkspaceSvg document.body.removeEventListener('wheel', this.dummyWheelListener); this.dummyWheelListener = null; } + + if (getFocusManager().isRegistered(this)) { + getFocusManager().unregisterTree(this); + } } /** @@ -2618,6 +2642,55 @@ export class WorkspaceSvg deltaY *= scale; this.scroll(this.scrollX + deltaX, this.scrollY + deltaY); } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + return this.svgGroup_; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableTree.getRootFocusableNode. */ + getRootFocusableNode(): IFocusableNode { + return this; + } + + /** See IFocusableTree.getRestoredFocusableNode. */ + getRestoredFocusableNode( + previousNode: IFocusableNode | null, + ): IFocusableNode | null { + if (!previousNode) { + return this.getTopBlocks(true)[0] ?? null; + } else return null; + } + + /** See IFocusableTree.getNestedTrees. */ + getNestedTrees(): Array { + return []; + } + + /** See IFocusableTree.lookUpFocusableNode. */ + lookUpFocusableNode(id: string): IFocusableNode | null { + return this.getBlockById(id) as IFocusableNode; + } + + /** See IFocusableTree.onTreeFocus. */ + onTreeFocus( + _node: IFocusableNode, + _previousTree: IFocusableTree | null, + ): void {} + + /** See IFocusableTree.onTreeBlur. */ + onTreeBlur(_nextTree: IFocusableTree | null): void {} } /** From 5bc83808bfe119a98de917f08840997dc9d3c324 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 24 Apr 2025 15:08:18 -0700 Subject: [PATCH 160/222] feat: Make toolbox and flyout focusable (#8920) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #8918 Fixes #8919 Fixes part of #8771 ### Proposed Changes This updates several classes in order to make toolboxes and flyouts focusable: - `IFlyout` is now an `IFocusableTree` with corresponding implementations in `FlyoutBase`. - `IToolbox` is now an `IFocusableTree` with corresponding implementations in `Toolbox`. - `IToolboxItem` is now an `IFocusableNode` with corresponding implementations in `ToolboxItem`. - As the primary toolbox items, `ToolboxCategory` and `ToolboxSeparator` were updated to have -1 tab indexes and defined IDs to help `ToolboxItem` fulfill its contracted for `IFocusableNode.getFocusableElement`. Each of these two new focusable trees have specific noteworthy behaviors behaviors: - `Toolbox` will automatically indicate that its first item should be focused (if one is present), even overriding the ability to focus the toolbox's root (however there are some cases where that can still happen). - `Toolbox` will automatically synchronize its selection state with its item nodes being focused. - `FlyoutBase`, now being a focusable tree, has had a tab index of 0 added. Normally a tab index of -1 is all that's needed, but the keyboard navigation plugin specifically uses 0 for flyout so that the flyout is tabbable. This is a **new** tab stop being introduced. - `FlyoutBase` holds a workspace (for rendering blocks) and, since `WorkspaceSvg` is already set up to be a focusable tree, it's represented as a subtree to `FlyoutBase`. This does introduce some wonky behaviors: the flyout's root will have passive focus while its contents have active focus. This could be manually disabled with some CSS if it ends up being a confusing user experience. - Both `FlyoutBase` and `WorkspaceSvg` have built-in behaviors for detecting when a user tries navigating away from an open flyout to ensure that the flyout is closed when it's supposed to be. That is, the flyout is auto-hideable and a non-flyout, non-toolbox node has then been focused. This matches parity with the `T`/`Esc` flows supported in the keyboard navigation plugin playground. One other thing to note: `Toolbox` had a few tests to update that were trying to reinit a toolbox without first disposing of it (which was caught by one of `FocusManager`'s state guardrails). ### Reason for Changes This is part of an ongoing effort to ensure key components of Blockly are focusable so that they can be keyboard-navigable (with other needed changes yet both in Core Blockly and the keyboard navigation plugin). ### Test Coverage No new tests have been added. It's certainly possible to add unit tests for the focusable configurations being introduced in this PR, but it may not be highly beneficial. It's largely assumed that the individual implementations should work due to a highly tested FocusManager, and it may be the case that the interactions of the components working together is far more important to verify (that is, the end user flows). The latter is planned to be tackled as part of #8915. ### Documentation No documentation changes should be needed here. ### Additional Information This includes changes that have been pulled from #8875. --- core/flyout_base.ts | 69 ++++++++++++++++++++++++++- core/interfaces/i_flyout.ts | 3 +- core/interfaces/i_toolbox.ts | 3 +- core/interfaces/i_toolbox_item.ts | 4 +- core/toolbox/category.ts | 2 + core/toolbox/separator.ts | 2 + core/toolbox/toolbox.ts | 77 ++++++++++++++++++++++++++++++- core/toolbox/toolbox_item.ts | 24 ++++++++++ core/workspace_svg.ts | 14 +++++- tests/mocha/toolbox_test.js | 3 ++ 10 files changed, 194 insertions(+), 7 deletions(-) diff --git a/core/flyout_base.ts b/core/flyout_base.ts index e738470a606..d24ea2758a0 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -21,9 +21,12 @@ import * as eventUtils from './events/utils.js'; import {FlyoutItem} from './flyout_item.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; +import {getFocusManager} from './focus_manager.js'; import {IAutoHideable} from './interfaces/i_autohideable.js'; import type {IFlyout} from './interfaces/i_flyout.js'; import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import {IFocusableNode} from './interfaces/i_focusable_node.js'; +import {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import * as renderManagement from './render_management.js'; @@ -43,7 +46,7 @@ import {WorkspaceSvg} from './workspace_svg.js'; */ export abstract class Flyout extends DeleteArea - implements IAutoHideable, IFlyout + implements IAutoHideable, IFlyout, IFocusableNode { /** * Position the flyout. @@ -303,6 +306,7 @@ export abstract class Flyout // hide/show code will set up proper visibility and size later. this.svgGroup_ = dom.createSvgElement(tagName, { 'class': 'blocklyFlyout', + 'tabindex': '0', }); this.svgGroup_.style.display = 'none'; this.svgBackground_ = dom.createSvgElement( @@ -317,6 +321,9 @@ export abstract class Flyout this.workspace_ .getThemeManager() .subscribe(this.svgBackground_, 'flyoutOpacity', 'fill-opacity'); + + getFocusManager().registerTree(this); + return this.svgGroup_; } @@ -398,6 +405,7 @@ export abstract class Flyout if (this.svgGroup_) { dom.removeNode(this.svgGroup_); } + getFocusManager().unregisterTree(this); } /** @@ -961,4 +969,63 @@ export abstract class Flyout return null; } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + if (!this.svgGroup_) throw new Error('Flyout DOM is not yet created.'); + return this.svgGroup_; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableTree.getRootFocusableNode. */ + getRootFocusableNode(): IFocusableNode { + return this; + } + + /** See IFocusableTree.getRestoredFocusableNode. */ + getRestoredFocusableNode( + _previousNode: IFocusableNode | null, + ): IFocusableNode | null { + return null; + } + + /** See IFocusableTree.getNestedTrees. */ + getNestedTrees(): Array { + return [this.workspace_]; + } + + /** See IFocusableTree.lookUpFocusableNode. */ + lookUpFocusableNode(_id: string): IFocusableNode | null { + // No focusable node needs to be returned since the flyout's subtree is a + // workspace that will manage its own focusable state. + return null; + } + + /** See IFocusableTree.onTreeFocus. */ + onTreeFocus( + _node: IFocusableNode, + _previousTree: IFocusableTree | null, + ): void {} + + /** See IFocusableTree.onTreeBlur. */ + onTreeBlur(nextTree: IFocusableTree | null): void { + const toolbox = this.targetWorkspace.getToolbox(); + // If focus is moving to either the toolbox or the flyout's workspace, do + // not close the flyout. For anything else, do close it since the flyout is + // no longer focused. + if (toolbox && nextTree === toolbox) return; + if (nextTree == this.workspace_) return; + if (toolbox) toolbox.clearSelection(); + this.autoHide(false); + } } diff --git a/core/interfaces/i_flyout.ts b/core/interfaces/i_flyout.ts index 42204775ece..067cd5ef20d 100644 --- a/core/interfaces/i_flyout.ts +++ b/core/interfaces/i_flyout.ts @@ -12,12 +12,13 @@ import type {Coordinate} from '../utils/coordinate.js'; import type {Svg} from '../utils/svg.js'; import type {FlyoutDefinition} from '../utils/toolbox.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; +import {IFocusableTree} from './i_focusable_tree.js'; import type {IRegistrable} from './i_registrable.js'; /** * Interface for a flyout. */ -export interface IFlyout extends IRegistrable { +export interface IFlyout extends IRegistrable, IFocusableTree { /** Whether the flyout is laid out horizontally or not. */ horizontalLayout: boolean; diff --git a/core/interfaces/i_toolbox.ts b/core/interfaces/i_toolbox.ts index 1780af94d8a..f5d9c9fd7c6 100644 --- a/core/interfaces/i_toolbox.ts +++ b/core/interfaces/i_toolbox.ts @@ -9,13 +9,14 @@ import type {ToolboxInfo} from '../utils/toolbox.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; import type {IFlyout} from './i_flyout.js'; +import type {IFocusableTree} from './i_focusable_tree.js'; import type {IRegistrable} from './i_registrable.js'; import type {IToolboxItem} from './i_toolbox_item.js'; /** * Interface for a toolbox. */ -export interface IToolbox extends IRegistrable { +export interface IToolbox extends IRegistrable, IFocusableTree { /** Initializes the toolbox. */ init(): void; diff --git a/core/interfaces/i_toolbox_item.ts b/core/interfaces/i_toolbox_item.ts index e3c9864f0c0..661624fd7e8 100644 --- a/core/interfaces/i_toolbox_item.ts +++ b/core/interfaces/i_toolbox_item.ts @@ -6,10 +6,12 @@ // Former goog.module ID: Blockly.IToolboxItem +import type {IFocusableNode} from './i_focusable_node.js'; + /** * Interface for an item in the toolbox. */ -export interface IToolboxItem { +export interface IToolboxItem extends IFocusableNode { /** * Initializes the toolbox item. * This includes creating the DOM and updating the state of any items based diff --git a/core/toolbox/category.ts b/core/toolbox/category.ts index d8ee8736ea6..fc7d1aa03cf 100644 --- a/core/toolbox/category.ts +++ b/core/toolbox/category.ts @@ -225,6 +225,8 @@ export class ToolboxCategory */ protected createContainer_(): HTMLDivElement { const container = document.createElement('div'); + container.tabIndex = -1; + container.id = this.getId(); const className = this.cssConfig_['container']; if (className) { dom.addClass(container, className); diff --git a/core/toolbox/separator.ts b/core/toolbox/separator.ts index 31ccb7e42f3..44ae358cf53 100644 --- a/core/toolbox/separator.ts +++ b/core/toolbox/separator.ts @@ -54,6 +54,8 @@ export class ToolboxSeparator extends ToolboxItem { */ protected createDom_(): HTMLDivElement { const container = document.createElement('div'); + container.tabIndex = -1; + container.id = this.getId(); const className = this.cssConfig_['container']; if (className) { dom.addClass(container, className); diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index b0fd82e97f2..ceb756afbd6 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -22,11 +22,14 @@ import {DeleteArea} from '../delete_area.js'; import '../events/events_toolbox_item_select.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; +import {getFocusManager} from '../focus_manager.js'; import type {IAutoHideable} from '../interfaces/i_autohideable.js'; import type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js'; import {isDeletable} from '../interfaces/i_deletable.js'; import type {IDraggable} from '../interfaces/i_draggable.js'; import type {IFlyout} from '../interfaces/i_flyout.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import type {IKeyboardAccessible} from '../interfaces/i_keyboard_accessible.js'; import type {ISelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.js'; import {isSelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.js'; @@ -51,7 +54,12 @@ import {CollapsibleToolboxCategory} from './collapsible_category.js'; */ export class Toolbox extends DeleteArea - implements IAutoHideable, IKeyboardAccessible, IStyleable, IToolbox + implements + IAutoHideable, + IKeyboardAccessible, + IStyleable, + IToolbox, + IFocusableNode { /** * The unique ID for this component that is used to register with the @@ -163,6 +171,7 @@ export class Toolbox ComponentManager.Capability.DRAG_TARGET, ], }); + getFocusManager().registerTree(this); } /** @@ -177,7 +186,6 @@ export class Toolbox const container = this.createContainer_(); this.contentsDiv_ = this.createContentsContainer_(); - this.contentsDiv_.tabIndex = 0; aria.setRole(this.contentsDiv_, aria.Role.TREE); container.appendChild(this.contentsDiv_); @@ -194,6 +202,7 @@ export class Toolbox */ protected createContainer_(): HTMLDivElement { const toolboxContainer = document.createElement('div'); + toolboxContainer.tabIndex = 0; toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v'); dom.addClass(toolboxContainer, 'blocklyToolbox'); toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); @@ -1077,7 +1086,71 @@ export class Toolbox this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv); dom.removeNode(this.HtmlDiv); } + + getFocusManager().unregisterTree(this); + } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + if (!this.HtmlDiv) throw Error('Toolbox DOM has not yet been created.'); + return this.HtmlDiv; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableTree.getRootFocusableNode. */ + getRootFocusableNode(): IFocusableNode { + return this; + } + + /** See IFocusableTree.getRestoredFocusableNode. */ + getRestoredFocusableNode( + previousNode: IFocusableNode | null, + ): IFocusableNode | null { + // Always try to select the first selectable toolbox item rather than the + // root of the toolbox. + if (!previousNode || previousNode === this) { + return this.getToolboxItems().find((item) => item.isSelectable()) ?? null; + } + return null; } + + /** See IFocusableTree.getNestedTrees. */ + getNestedTrees(): Array { + return []; + } + + /** See IFocusableTree.lookUpFocusableNode. */ + lookUpFocusableNode(id: string): IFocusableNode | null { + return this.getToolboxItemById(id) as IFocusableNode; + } + + /** See IFocusableTree.onTreeFocus. */ + onTreeFocus( + node: IFocusableNode, + _previousTree: IFocusableTree | null, + ): void { + if (node !== this) { + // Only select the item if it isn't already selected so as to not toggle. + if (this.getSelectedItem() !== node) { + this.setSelectedItem(node as IToolboxItem); + } + } else { + this.clearSelection(); + } + } + + /** See IFocusableTree.onTreeBlur. */ + onTreeBlur(_nextTree: IFocusableTree | null): void {} } /** CSS for Toolbox. See css.js for use. */ diff --git a/core/toolbox/toolbox_item.ts b/core/toolbox/toolbox_item.ts index ef9d979ab43..0d46a5eadfd 100644 --- a/core/toolbox/toolbox_item.ts +++ b/core/toolbox/toolbox_item.ts @@ -12,6 +12,7 @@ // Former goog.module ID: Blockly.ToolboxItem import type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import type {IToolbox} from '../interfaces/i_toolbox.js'; import type {IToolboxItem} from '../interfaces/i_toolbox_item.js'; import * as idGenerator from '../utils/idgenerator.js'; @@ -148,5 +149,28 @@ export class ToolboxItem implements IToolboxItem { * @param _isVisible True if category should be visible. */ setVisible_(_isVisible: boolean) {} + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + const div = this.getDiv(); + if (!div) { + throw Error('Trying to access toolbox item before DOM is initialized.'); + } + if (!(div instanceof HTMLElement)) { + throw Error('Toolbox item div is unexpectedly not an HTML element.'); + } + return div as HTMLElement; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.parentToolbox_; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} } // nop by default diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index b1c96373771..250d6cf43e2 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2690,7 +2690,19 @@ export class WorkspaceSvg ): void {} /** See IFocusableTree.onTreeBlur. */ - onTreeBlur(_nextTree: IFocusableTree | null): void {} + onTreeBlur(nextTree: IFocusableTree | null): void { + // If the flyout loses focus, make sure to close it. + if (this.isFlyout && this.targetWorkspace) { + // Only hide the flyout if the flyout's workspace is losing focus and that + // focus isn't returning to the flyout itself or the toolbox. + const flyout = this.targetWorkspace.getFlyout(); + const toolbox = this.targetWorkspace.getToolbox(); + if (flyout && nextTree === flyout) return; + if (toolbox && nextTree === toolbox) return; + if (toolbox) toolbox.clearSelection(); + if (flyout && flyout instanceof Flyout) flyout.autoHide(false); + } + } } /** diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js index 10bfd335223..f32319c6779 100644 --- a/tests/mocha/toolbox_test.js +++ b/tests/mocha/toolbox_test.js @@ -54,6 +54,7 @@ suite('Toolbox', function () { const themeManagerSpy = sinon.spy(themeManager, 'subscribe'); const componentManager = this.toolbox.workspace_.getComponentManager(); sinon.stub(componentManager, 'addComponent'); + this.toolbox.dispose(); // Dispose of the old toolbox so that it can be reinited. this.toolbox.init(); sinon.assert.calledWith( themeManagerSpy, @@ -72,12 +73,14 @@ suite('Toolbox', function () { const renderSpy = sinon.spy(this.toolbox, 'render'); const componentManager = this.toolbox.workspace_.getComponentManager(); sinon.stub(componentManager, 'addComponent'); + this.toolbox.dispose(); // Dispose of the old toolbox so that it can be reinited. this.toolbox.init(); sinon.assert.calledOnce(renderSpy); }); test('Init called -> Flyout is initialized', function () { const componentManager = this.toolbox.workspace_.getComponentManager(); sinon.stub(componentManager, 'addComponent'); + this.toolbox.dispose(); // Dispose of the old toolbox so that it can be reinited. this.toolbox.init(); assert.isDefined(this.toolbox.getFlyout()); }); From 8f59649956346440b417fda861e00d8375fa8be2 Mon Sep 17 00:00:00 2001 From: Grace <145345672+microbit-grace@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:26:58 +0100 Subject: [PATCH 161/222] fix: LineCursor can loop forward, but not back (#8926) * fix: loop cursor when moving to prev node * chore: add loop tests for LineCursor prev and out * chore: fix out loop test for line cursor --- core/keyboard_nav/line_cursor.ts | 7 +++++-- tests/mocha/cursor_test.js | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index b2bda39c739..2025da7bf06 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -146,10 +146,12 @@ export class LineCursor extends Marker { if (!curNode) { return null; } - const newNode = this.getPreviousNodeImpl( + const newNode = this.getPreviousNode( curNode, this.validLineNode.bind(this), + true, ); + if (newNode) { this.setCurNode(newNode); } @@ -168,9 +170,10 @@ export class LineCursor extends Marker { if (!curNode) { return null; } - const newNode = this.getPreviousNodeImpl( + const newNode = this.getPreviousNode( curNode, this.validInLineNode.bind(this), + true, ); if (newNode) { diff --git a/tests/mocha/cursor_test.js b/tests/mocha/cursor_test.js index 905f48c09ad..53f0714da11 100644 --- a/tests/mocha/cursor_test.js +++ b/tests/mocha/cursor_test.js @@ -125,6 +125,15 @@ suite('Cursor', function () { assert.equal(curNode.getLocation(), this.blocks.A.nextConnection); }); + test('Prev - From first connection loop to last next connection', function () { + const prevConnection = this.blocks.A.previousConnection; + const prevConnectionNode = ASTNode.createConnectionNode(prevConnection); + this.cursor.setCurNode(prevConnectionNode); + this.cursor.prev(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode.getLocation(), this.blocks.D.nextConnection); + }); + test('Out - From field does not skip over block node', function () { const field = this.blocks.E.inputList[0].fieldRow[0]; const fieldNode = ASTNode.createFieldNode(field); @@ -133,6 +142,15 @@ suite('Cursor', function () { const curNode = this.cursor.getCurNode(); assert.equal(curNode.getLocation(), this.blocks.E); }); + + test('Out - From first connection loop to last next connection', function () { + const prevConnection = this.blocks.A.previousConnection; + const prevConnectionNode = ASTNode.createConnectionNode(prevConnection); + this.cursor.setCurNode(prevConnectionNode); + this.cursor.out(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode.getLocation(), this.blocks.D.nextConnection); + }); }); suite('Searching', function () { setup(function () { From ada01da23ce84f359436c06181ca4ba41f5253c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 10:37:20 -0700 Subject: [PATCH 162/222] chore(deps): bump jsdom from 25.0.1 to 26.1.0 (#8887) Bumps [jsdom](https://github.com/jsdom/jsdom) from 25.0.1 to 26.1.0. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Changelog](https://github.com/jsdom/jsdom/blob/main/Changelog.md) - [Commits](https://github.com/jsdom/jsdom/compare/25.0.1...26.1.0) --- updated-dependencies: - dependency-name: jsdom dependency-version: 26.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 347 +++++++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 205 insertions(+), 144 deletions(-) diff --git a/package-lock.json b/package-lock.json index bcd046c05e5..4fbcf8f5bdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "jsdom": "25.0.1" + "jsdom": "26.1.0" }, "devDependencies": { "@blockly/block-test": "^6.0.4", @@ -69,6 +69,25 @@ "node": ">=0.10.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.1.tgz", + "integrity": "sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.2", + "@csstools/css-color-parser": "^3.0.8", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/@blockly/block-test": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-6.0.11.tgz", @@ -235,6 +254,116 @@ "blockly": "^11.0.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz", + "integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz", + "integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.49.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz", @@ -2429,11 +2558,6 @@ "node": ">= 10.13.0" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -2939,17 +3063,6 @@ "color-support": "bin.js" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -3363,11 +3476,13 @@ } }, "node_modules/cssstyle": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", - "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.0.tgz", + "integrity": "sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==", + "license": "MIT", "dependencies": { - "rrweb-cssom": "^0.7.1" + "@asamuzakjp/css-color": "^3.1.1", + "rrweb-cssom": "^0.8.0" }, "engines": { "node": ">=18" @@ -3411,29 +3526,6 @@ "node": ">=18" } }, - "node_modules/data-urls/node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", - "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -3484,9 +3576,10 @@ } }, "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "license": "MIT" }, "node_modules/decode-uri-component": { "version": "0.2.2", @@ -3537,14 +3630,6 @@ "node": ">= 14" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4591,19 +4676,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -6085,29 +6157,29 @@ } }, "node_modules/jsdom": { - "version": "25.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", - "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", "dependencies": { - "cssstyle": "^4.1.0", + "cssstyle": "^4.2.1", "data-urls": "^5.0.0", - "decimal.js": "^10.4.3", - "form-data": "^4.0.0", + "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.12", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.7.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^5.0.0", + "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0", + "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, @@ -6115,7 +6187,7 @@ "node": ">=18" }, "peerDependencies": { - "canvas": "^2.11.2" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -6134,17 +6206,6 @@ "node": ">=18" } }, - "node_modules/jsdom/node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/jsdom/node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -6156,18 +6217,6 @@ "node": ">=18" } }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", - "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6643,25 +6692,6 @@ "node": ">=4" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6948,9 +6978,10 @@ } }, "node_modules/nwsapi": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", - "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==" + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", @@ -7184,11 +7215,12 @@ } }, "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", "dependencies": { - "entities": "^4.4.0" + "entities": "^4.5.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -8051,9 +8083,10 @@ } }, "node_modules/rrweb-cssom": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", - "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" }, "node_modules/run-parallel": { "version": "1.2.0", @@ -8725,20 +8758,22 @@ } }, "node_modules/tldts": { - "version": "6.1.48", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.48.tgz", - "integrity": "sha512-SPbnh1zaSzi/OsmHb1vrPNnYuwJbdWjwo5TbBYYMlTtH3/1DSb41t8bcSxkwDmmbG2q6VLPVvQc7Yf23T+1EEw==", + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", "dependencies": { - "tldts-core": "^6.1.48" + "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.48", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.48.tgz", - "integrity": "sha512-3gD9iKn/n2UuFH1uilBviK9gvTNT6iYwdqrj1Vr5mh8FuelvpRNaYVH4pNYqUgOGU4aAdL9X35eLuuj0gRsx+A==" + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" }, "node_modules/tmp": { "version": "0.0.33", @@ -8777,9 +8812,10 @@ } }, "node_modules/tough-cookie": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", - "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", "dependencies": { "tldts": "^6.1.32" }, @@ -8787,6 +8823,18 @@ "node": ">=16" } }, + "node_modules/tr46": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz", + "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -9329,6 +9377,19 @@ "node": ">=18" } }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 477ccad6926..48aa56779d7 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "yargs": "^17.2.1" }, "dependencies": { - "jsdom": "25.0.1" + "jsdom": "26.1.0" }, "engines": { "node": ">=18" From c644fe36eff50de21d485fe22127307b52a0a74a Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Fri, 25 Apr 2025 15:03:32 -0700 Subject: [PATCH 163/222] Fix: Revert focus prs (#8933) * Revert "feat: Make toolbox and flyout focusable (#8920)" This reverts commit 5bc83808bfe119a98de917f08840997dc9d3c324. * Revert "feat: Make WorkspaceSvg and BlockSvg focusable (#8916)" This reverts commit d7680cf32ec0fff5b1fabf8d786f996e9e454ff2. --- core/block_svg.ts | 28 +-------- core/flyout_base.ts | 69 +-------------------- core/inject.ts | 5 ++ core/interfaces/i_flyout.ts | 3 +- core/interfaces/i_toolbox.ts | 3 +- core/interfaces/i_toolbox_item.ts | 4 +- core/renderers/common/path_object.ts | 2 +- core/toolbox/category.ts | 2 - core/toolbox/separator.ts | 2 - core/toolbox/toolbox.ts | 77 +----------------------- core/toolbox/toolbox_item.ts | 24 -------- core/workspace_svg.ts | 89 +--------------------------- tests/mocha/toolbox_test.js | 3 - 13 files changed, 15 insertions(+), 296 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 8bc0b7af3f6..b8712b01914 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -44,8 +44,6 @@ import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {ICopyable} from './interfaces/i_copyable.js'; import {IDeletable} from './interfaces/i_deletable.js'; import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js'; -import type {IFocusableNode} from './interfaces/i_focusable_node.js'; -import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {IIcon} from './interfaces/i_icon.js'; import * as internalConstants from './internal_constants.js'; import {MarkerManager} from './marker_manager.js'; @@ -78,8 +76,7 @@ export class BlockSvg IContextMenu, ICopyable, IDraggable, - IDeletable, - IFocusableNode + IDeletable { /** * Constant for identifying rows that are to be rendered inline. @@ -213,7 +210,6 @@ export class BlockSvg // Expose this block's ID on its top-level SVG group. this.svgGroup.setAttribute('data-id', this.id); - svgPath.id = this.id; this.doInit_(); } @@ -1823,26 +1819,4 @@ export class BlockSvg ); } } - - /** See IFocusableNode.getFocusableElement. */ - getFocusableElement(): HTMLElement | SVGElement { - return this.pathObject.svgPath; - } - - /** See IFocusableNode.getFocusableTree. */ - getFocusableTree(): IFocusableTree { - return this.workspace; - } - - /** See IFocusableNode.onNodeFocus. */ - onNodeFocus(): void { - common.setSelected(this); - } - - /** See IFocusableNode.onNodeBlur. */ - onNodeBlur(): void { - if (common.getSelected() === this) { - common.setSelected(null); - } - } } diff --git a/core/flyout_base.ts b/core/flyout_base.ts index d24ea2758a0..e738470a606 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -21,12 +21,9 @@ import * as eventUtils from './events/utils.js'; import {FlyoutItem} from './flyout_item.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; -import {getFocusManager} from './focus_manager.js'; import {IAutoHideable} from './interfaces/i_autohideable.js'; import type {IFlyout} from './interfaces/i_flyout.js'; import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; -import {IFocusableNode} from './interfaces/i_focusable_node.js'; -import {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import * as renderManagement from './render_management.js'; @@ -46,7 +43,7 @@ import {WorkspaceSvg} from './workspace_svg.js'; */ export abstract class Flyout extends DeleteArea - implements IAutoHideable, IFlyout, IFocusableNode + implements IAutoHideable, IFlyout { /** * Position the flyout. @@ -306,7 +303,6 @@ export abstract class Flyout // hide/show code will set up proper visibility and size later. this.svgGroup_ = dom.createSvgElement(tagName, { 'class': 'blocklyFlyout', - 'tabindex': '0', }); this.svgGroup_.style.display = 'none'; this.svgBackground_ = dom.createSvgElement( @@ -321,9 +317,6 @@ export abstract class Flyout this.workspace_ .getThemeManager() .subscribe(this.svgBackground_, 'flyoutOpacity', 'fill-opacity'); - - getFocusManager().registerTree(this); - return this.svgGroup_; } @@ -405,7 +398,6 @@ export abstract class Flyout if (this.svgGroup_) { dom.removeNode(this.svgGroup_); } - getFocusManager().unregisterTree(this); } /** @@ -969,63 +961,4 @@ export abstract class Flyout return null; } - - /** See IFocusableNode.getFocusableElement. */ - getFocusableElement(): HTMLElement | SVGElement { - if (!this.svgGroup_) throw new Error('Flyout DOM is not yet created.'); - return this.svgGroup_; - } - - /** See IFocusableNode.getFocusableTree. */ - getFocusableTree(): IFocusableTree { - return this; - } - - /** See IFocusableNode.onNodeFocus. */ - onNodeFocus(): void {} - - /** See IFocusableNode.onNodeBlur. */ - onNodeBlur(): void {} - - /** See IFocusableTree.getRootFocusableNode. */ - getRootFocusableNode(): IFocusableNode { - return this; - } - - /** See IFocusableTree.getRestoredFocusableNode. */ - getRestoredFocusableNode( - _previousNode: IFocusableNode | null, - ): IFocusableNode | null { - return null; - } - - /** See IFocusableTree.getNestedTrees. */ - getNestedTrees(): Array { - return [this.workspace_]; - } - - /** See IFocusableTree.lookUpFocusableNode. */ - lookUpFocusableNode(_id: string): IFocusableNode | null { - // No focusable node needs to be returned since the flyout's subtree is a - // workspace that will manage its own focusable state. - return null; - } - - /** See IFocusableTree.onTreeFocus. */ - onTreeFocus( - _node: IFocusableNode, - _previousTree: IFocusableTree | null, - ): void {} - - /** See IFocusableTree.onTreeBlur. */ - onTreeBlur(nextTree: IFocusableTree | null): void { - const toolbox = this.targetWorkspace.getToolbox(); - // If focus is moving to either the toolbox or the flyout's workspace, do - // not close the flyout. For anything else, do close it since the flyout is - // no longer focused. - if (toolbox && nextTree === toolbox) return; - if (nextTree == this.workspace_) return; - if (toolbox) toolbox.clearSelection(); - this.autoHide(false); - } } diff --git a/core/inject.ts b/core/inject.ts index 34d9c1795f8..de78fbfae75 100644 --- a/core/inject.ts +++ b/core/inject.ts @@ -13,11 +13,13 @@ import * as common from './common.js'; import * as Css from './css.js'; import * as dropDownDiv from './dropdowndiv.js'; import {Grid} from './grid.js'; +import {Msg} from './msg.js'; import {Options} from './options.js'; import {ScrollbarPair} from './scrollbar_pair.js'; import {ShortcutRegistry} from './shortcut_registry.js'; import * as Tooltip from './tooltip.js'; import * as Touch from './touch.js'; +import * as aria from './utils/aria.js'; import * as dom from './utils/dom.js'; import {Svg} from './utils/svg.js'; import * as WidgetDiv from './widgetdiv.js'; @@ -54,6 +56,8 @@ export function inject( if (opt_options?.rtl) { dom.addClass(subContainer, 'blocklyRTL'); } + subContainer.tabIndex = 0; + aria.setState(subContainer, aria.State.LABEL, Msg['WORKSPACE_ARIA_LABEL']); containerElement!.appendChild(subContainer); const svg = createDom(subContainer, options); @@ -122,6 +126,7 @@ function createDom(container: HTMLElement, options: Options): SVGElement { 'xmlns:xlink': dom.XLINK_NS, 'version': '1.1', 'class': 'blocklySvg', + 'tabindex': '0', }, container, ); diff --git a/core/interfaces/i_flyout.ts b/core/interfaces/i_flyout.ts index 067cd5ef20d..42204775ece 100644 --- a/core/interfaces/i_flyout.ts +++ b/core/interfaces/i_flyout.ts @@ -12,13 +12,12 @@ import type {Coordinate} from '../utils/coordinate.js'; import type {Svg} from '../utils/svg.js'; import type {FlyoutDefinition} from '../utils/toolbox.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; -import {IFocusableTree} from './i_focusable_tree.js'; import type {IRegistrable} from './i_registrable.js'; /** * Interface for a flyout. */ -export interface IFlyout extends IRegistrable, IFocusableTree { +export interface IFlyout extends IRegistrable { /** Whether the flyout is laid out horizontally or not. */ horizontalLayout: boolean; diff --git a/core/interfaces/i_toolbox.ts b/core/interfaces/i_toolbox.ts index f5d9c9fd7c6..1780af94d8a 100644 --- a/core/interfaces/i_toolbox.ts +++ b/core/interfaces/i_toolbox.ts @@ -9,14 +9,13 @@ import type {ToolboxInfo} from '../utils/toolbox.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; import type {IFlyout} from './i_flyout.js'; -import type {IFocusableTree} from './i_focusable_tree.js'; import type {IRegistrable} from './i_registrable.js'; import type {IToolboxItem} from './i_toolbox_item.js'; /** * Interface for a toolbox. */ -export interface IToolbox extends IRegistrable, IFocusableTree { +export interface IToolbox extends IRegistrable { /** Initializes the toolbox. */ init(): void; diff --git a/core/interfaces/i_toolbox_item.ts b/core/interfaces/i_toolbox_item.ts index 661624fd7e8..e3c9864f0c0 100644 --- a/core/interfaces/i_toolbox_item.ts +++ b/core/interfaces/i_toolbox_item.ts @@ -6,12 +6,10 @@ // Former goog.module ID: Blockly.IToolboxItem -import type {IFocusableNode} from './i_focusable_node.js'; - /** * Interface for an item in the toolbox. */ -export interface IToolboxItem extends IFocusableNode { +export interface IToolboxItem { /** * Initializes the toolbox item. * This includes creating the DOM and updating the state of any items based diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index 72cf2a594ce..077f80bb741 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -62,7 +62,7 @@ export class PathObject implements IPathObject { /** The primary path of the block. */ this.svgPath = dom.createSvgElement( Svg.PATH, - {'class': 'blocklyPath', 'tabindex': '-1'}, + {'class': 'blocklyPath'}, this.svgRoot, ); diff --git a/core/toolbox/category.ts b/core/toolbox/category.ts index fc7d1aa03cf..d8ee8736ea6 100644 --- a/core/toolbox/category.ts +++ b/core/toolbox/category.ts @@ -225,8 +225,6 @@ export class ToolboxCategory */ protected createContainer_(): HTMLDivElement { const container = document.createElement('div'); - container.tabIndex = -1; - container.id = this.getId(); const className = this.cssConfig_['container']; if (className) { dom.addClass(container, className); diff --git a/core/toolbox/separator.ts b/core/toolbox/separator.ts index 44ae358cf53..31ccb7e42f3 100644 --- a/core/toolbox/separator.ts +++ b/core/toolbox/separator.ts @@ -54,8 +54,6 @@ export class ToolboxSeparator extends ToolboxItem { */ protected createDom_(): HTMLDivElement { const container = document.createElement('div'); - container.tabIndex = -1; - container.id = this.getId(); const className = this.cssConfig_['container']; if (className) { dom.addClass(container, className); diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index ceb756afbd6..b0fd82e97f2 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -22,14 +22,11 @@ import {DeleteArea} from '../delete_area.js'; import '../events/events_toolbox_item_select.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; -import {getFocusManager} from '../focus_manager.js'; import type {IAutoHideable} from '../interfaces/i_autohideable.js'; import type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js'; import {isDeletable} from '../interfaces/i_deletable.js'; import type {IDraggable} from '../interfaces/i_draggable.js'; import type {IFlyout} from '../interfaces/i_flyout.js'; -import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; -import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import type {IKeyboardAccessible} from '../interfaces/i_keyboard_accessible.js'; import type {ISelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.js'; import {isSelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.js'; @@ -54,12 +51,7 @@ import {CollapsibleToolboxCategory} from './collapsible_category.js'; */ export class Toolbox extends DeleteArea - implements - IAutoHideable, - IKeyboardAccessible, - IStyleable, - IToolbox, - IFocusableNode + implements IAutoHideable, IKeyboardAccessible, IStyleable, IToolbox { /** * The unique ID for this component that is used to register with the @@ -171,7 +163,6 @@ export class Toolbox ComponentManager.Capability.DRAG_TARGET, ], }); - getFocusManager().registerTree(this); } /** @@ -186,6 +177,7 @@ export class Toolbox const container = this.createContainer_(); this.contentsDiv_ = this.createContentsContainer_(); + this.contentsDiv_.tabIndex = 0; aria.setRole(this.contentsDiv_, aria.Role.TREE); container.appendChild(this.contentsDiv_); @@ -202,7 +194,6 @@ export class Toolbox */ protected createContainer_(): HTMLDivElement { const toolboxContainer = document.createElement('div'); - toolboxContainer.tabIndex = 0; toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v'); dom.addClass(toolboxContainer, 'blocklyToolbox'); toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); @@ -1086,71 +1077,7 @@ export class Toolbox this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv); dom.removeNode(this.HtmlDiv); } - - getFocusManager().unregisterTree(this); - } - - /** See IFocusableNode.getFocusableElement. */ - getFocusableElement(): HTMLElement | SVGElement { - if (!this.HtmlDiv) throw Error('Toolbox DOM has not yet been created.'); - return this.HtmlDiv; - } - - /** See IFocusableNode.getFocusableTree. */ - getFocusableTree(): IFocusableTree { - return this; - } - - /** See IFocusableNode.onNodeFocus. */ - onNodeFocus(): void {} - - /** See IFocusableNode.onNodeBlur. */ - onNodeBlur(): void {} - - /** See IFocusableTree.getRootFocusableNode. */ - getRootFocusableNode(): IFocusableNode { - return this; - } - - /** See IFocusableTree.getRestoredFocusableNode. */ - getRestoredFocusableNode( - previousNode: IFocusableNode | null, - ): IFocusableNode | null { - // Always try to select the first selectable toolbox item rather than the - // root of the toolbox. - if (!previousNode || previousNode === this) { - return this.getToolboxItems().find((item) => item.isSelectable()) ?? null; - } - return null; } - - /** See IFocusableTree.getNestedTrees. */ - getNestedTrees(): Array { - return []; - } - - /** See IFocusableTree.lookUpFocusableNode. */ - lookUpFocusableNode(id: string): IFocusableNode | null { - return this.getToolboxItemById(id) as IFocusableNode; - } - - /** See IFocusableTree.onTreeFocus. */ - onTreeFocus( - node: IFocusableNode, - _previousTree: IFocusableTree | null, - ): void { - if (node !== this) { - // Only select the item if it isn't already selected so as to not toggle. - if (this.getSelectedItem() !== node) { - this.setSelectedItem(node as IToolboxItem); - } - } else { - this.clearSelection(); - } - } - - /** See IFocusableTree.onTreeBlur. */ - onTreeBlur(_nextTree: IFocusableTree | null): void {} } /** CSS for Toolbox. See css.js for use. */ diff --git a/core/toolbox/toolbox_item.ts b/core/toolbox/toolbox_item.ts index 0d46a5eadfd..ef9d979ab43 100644 --- a/core/toolbox/toolbox_item.ts +++ b/core/toolbox/toolbox_item.ts @@ -12,7 +12,6 @@ // Former goog.module ID: Blockly.ToolboxItem import type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js'; -import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import type {IToolbox} from '../interfaces/i_toolbox.js'; import type {IToolboxItem} from '../interfaces/i_toolbox_item.js'; import * as idGenerator from '../utils/idgenerator.js'; @@ -149,28 +148,5 @@ export class ToolboxItem implements IToolboxItem { * @param _isVisible True if category should be visible. */ setVisible_(_isVisible: boolean) {} - - /** See IFocusableNode.getFocusableElement. */ - getFocusableElement(): HTMLElement | SVGElement { - const div = this.getDiv(); - if (!div) { - throw Error('Trying to access toolbox item before DOM is initialized.'); - } - if (!(div instanceof HTMLElement)) { - throw Error('Toolbox item div is unexpectedly not an HTML element.'); - } - return div as HTMLElement; - } - - /** See IFocusableNode.getFocusableTree. */ - getFocusableTree(): IFocusableTree { - return this.parentToolbox_; - } - - /** See IFocusableNode.onNodeFocus. */ - onNodeFocus(): void {} - - /** See IFocusableNode.onNodeBlur. */ - onNodeBlur(): void {} } // nop by default diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 250d6cf43e2..91668b744d4 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -37,7 +37,6 @@ import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import {Flyout} from './flyout_base.js'; import type {FlyoutButton} from './flyout_button.js'; -import {getFocusManager} from './focus_manager.js'; import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; @@ -45,8 +44,6 @@ import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {IDragTarget} from './interfaces/i_drag_target.js'; import type {IFlyout} from './interfaces/i_flyout.js'; -import type {IFocusableNode} from './interfaces/i_focusable_node.js'; -import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; import type {IToolbox} from './interfaces/i_toolbox.js'; import type { @@ -57,7 +54,6 @@ import type {LineCursor} from './keyboard_nav/line_cursor.js'; import type {Marker} from './keyboard_nav/marker.js'; import {LayerManager} from './layer_manager.js'; import {MarkerManager} from './marker_manager.js'; -import {Msg} from './msg.js'; import {Options} from './options.js'; import * as Procedures from './procedures.js'; import * as registry from './registry.js'; @@ -70,7 +66,6 @@ import {Classic} from './theme/classic.js'; import {ThemeManager} from './theme_manager.js'; import * as Tooltip from './tooltip.js'; import type {Trashcan} from './trashcan.js'; -import * as aria from './utils/aria.js'; import * as arrayUtils from './utils/array.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; @@ -98,7 +93,7 @@ const ZOOM_TO_FIT_MARGIN = 20; */ export class WorkspaceSvg extends Workspace - implements IASTNodeLocationSvg, IContextMenu, IFocusableNode, IFocusableTree + implements IASTNodeLocationSvg, IContextMenu { /** * A wrapper function called when a resize event occurs. @@ -769,19 +764,7 @@ export class WorkspaceSvg * * */ - this.svgGroup_ = dom.createSvgElement(Svg.G, { - 'class': 'blocklyWorkspace', - // Only the top-level workspace should be tabbable. - 'tabindex': injectionDiv ? '0' : '-1', - 'id': this.id, - }); - if (injectionDiv) { - aria.setState( - this.svgGroup_, - aria.State.LABEL, - Msg['WORKSPACE_ARIA_LABEL'], - ); - } + this.svgGroup_ = dom.createSvgElement(Svg.G, {'class': 'blocklyWorkspace'}); // Note that a alone does not receive mouse events--it must have a // valid target inside it. If no background class is specified, as in the @@ -857,9 +840,6 @@ export class WorkspaceSvg this.getTheme(), isParentWorkspace ? this.getInjectionDiv() : undefined, ); - - getFocusManager().registerTree(this); - return this.svgGroup_; } @@ -944,10 +924,6 @@ export class WorkspaceSvg document.body.removeEventListener('wheel', this.dummyWheelListener); this.dummyWheelListener = null; } - - if (getFocusManager().isRegistered(this)) { - getFocusManager().unregisterTree(this); - } } /** @@ -2642,67 +2618,6 @@ export class WorkspaceSvg deltaY *= scale; this.scroll(this.scrollX + deltaX, this.scrollY + deltaY); } - - /** See IFocusableNode.getFocusableElement. */ - getFocusableElement(): HTMLElement | SVGElement { - return this.svgGroup_; - } - - /** See IFocusableNode.getFocusableTree. */ - getFocusableTree(): IFocusableTree { - return this; - } - - /** See IFocusableNode.onNodeFocus. */ - onNodeFocus(): void {} - - /** See IFocusableNode.onNodeBlur. */ - onNodeBlur(): void {} - - /** See IFocusableTree.getRootFocusableNode. */ - getRootFocusableNode(): IFocusableNode { - return this; - } - - /** See IFocusableTree.getRestoredFocusableNode. */ - getRestoredFocusableNode( - previousNode: IFocusableNode | null, - ): IFocusableNode | null { - if (!previousNode) { - return this.getTopBlocks(true)[0] ?? null; - } else return null; - } - - /** See IFocusableTree.getNestedTrees. */ - getNestedTrees(): Array { - return []; - } - - /** See IFocusableTree.lookUpFocusableNode. */ - lookUpFocusableNode(id: string): IFocusableNode | null { - return this.getBlockById(id) as IFocusableNode; - } - - /** See IFocusableTree.onTreeFocus. */ - onTreeFocus( - _node: IFocusableNode, - _previousTree: IFocusableTree | null, - ): void {} - - /** See IFocusableTree.onTreeBlur. */ - onTreeBlur(nextTree: IFocusableTree | null): void { - // If the flyout loses focus, make sure to close it. - if (this.isFlyout && this.targetWorkspace) { - // Only hide the flyout if the flyout's workspace is losing focus and that - // focus isn't returning to the flyout itself or the toolbox. - const flyout = this.targetWorkspace.getFlyout(); - const toolbox = this.targetWorkspace.getToolbox(); - if (flyout && nextTree === flyout) return; - if (toolbox && nextTree === toolbox) return; - if (toolbox) toolbox.clearSelection(); - if (flyout && flyout instanceof Flyout) flyout.autoHide(false); - } - } } /** diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js index f32319c6779..10bfd335223 100644 --- a/tests/mocha/toolbox_test.js +++ b/tests/mocha/toolbox_test.js @@ -54,7 +54,6 @@ suite('Toolbox', function () { const themeManagerSpy = sinon.spy(themeManager, 'subscribe'); const componentManager = this.toolbox.workspace_.getComponentManager(); sinon.stub(componentManager, 'addComponent'); - this.toolbox.dispose(); // Dispose of the old toolbox so that it can be reinited. this.toolbox.init(); sinon.assert.calledWith( themeManagerSpy, @@ -73,14 +72,12 @@ suite('Toolbox', function () { const renderSpy = sinon.spy(this.toolbox, 'render'); const componentManager = this.toolbox.workspace_.getComponentManager(); sinon.stub(componentManager, 'addComponent'); - this.toolbox.dispose(); // Dispose of the old toolbox so that it can be reinited. this.toolbox.init(); sinon.assert.calledOnce(renderSpy); }); test('Init called -> Flyout is initialized', function () { const componentManager = this.toolbox.workspace_.getComponentManager(); sinon.stub(componentManager, 'addComponent'); - this.toolbox.dispose(); // Dispose of the old toolbox so that it can be reinited. this.toolbox.init(); assert.isDefined(this.toolbox.getFlyout()); }); From dee27b905dd4cfbae1f564192d594e401c19b468 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Mon, 28 Apr 2025 15:24:57 -0700 Subject: [PATCH 164/222] fix: Support RTL in WorkspaceSvg.scrollIntoBounds (#8936) * feat: add support for RTL to scrollBoundsIntoView * Add additional comment --- core/workspace_svg.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 91668b744d4..e8d411651c5 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2603,15 +2603,28 @@ export class WorkspaceSvg let deltaY = 0; if (bounds.left < viewport.left) { - deltaX = viewport.left - bounds.left; + deltaX = this.RTL + ? Math.min( + viewport.left - bounds.left, + viewport.right - bounds.right, // Don't move the right side out of view + ) + : viewport.left - bounds.left; } else if (bounds.right > viewport.right) { - deltaX = viewport.right - bounds.right; + deltaX = this.RTL + ? viewport.right - bounds.right + : Math.max( + viewport.right - bounds.right, + viewport.left - bounds.left, // Don't move the left side out of view + ); } if (bounds.top < viewport.top) { deltaY = viewport.top - bounds.top; } else if (bounds.bottom > viewport.bottom) { - deltaY = viewport.bottom - bounds.bottom; + deltaY = Math.max( + viewport.bottom - bounds.bottom, + viewport.top - bounds.top, // Don't move the top out of view + ); } deltaX *= scale; From b9b40f48abff9d7d4ff352039d982639458c975f Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Tue, 29 Apr 2025 16:15:42 +0100 Subject: [PATCH 165/222] fix: Fix bug in BlockSvg.prototype.setParent (#8934) * test(BlockSvg): Add tests for setParent(null) when dragging Add tests for scenarios where block(s) unrelated to the block being disconnected has/have been marked as as being dragged. Due to a bug in BlockSvg.prototype.setParent, one of these fails in the case that the dragging block is not a top block. Also add a check to assertNonParentAndOrphan to check that the orphan block's SVG root's parent is the workspace's canvass (i.e., that orphan is a top-level block in the DOM too). * fix(BlockSvg): Fix bug in setParent re: dragging block Fix an incorrect assumption in setParent: the topmost block whose root SVG element has the blocklyDragging class may not actually be a top-level block. * refactor(BlockDragStrategy): Hide connection preview earlier * chore(BlockDragStrategy): prefer ?. to !. Per nit on PR #8934. * fix(BlockSvg): Spelling: "canvass" -> "canvas" --- core/block_svg.ts | 18 ++++++++++---- core/dragging/block_drag_strategy.ts | 18 +++++++------- tests/mocha/block_test.js | 37 ++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index b8712b01914..4cef79df8e6 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -305,14 +305,22 @@ export class BlockSvg (newParent as BlockSvg).getSvgRoot().appendChild(svgRoot); } else if (oldParent) { // If we are losing a parent, we want to move our DOM element to the - // root of the workspace. - const draggingBlock = this.workspace + // root of the workspace. Try to insert it before any top-level + // block being dragged, but note that blocks can have the + // blocklyDragging class even if they're not top blocks (especially + // at start and end of a drag). + const draggingBlockElement = this.workspace .getCanvas() .querySelector('.blocklyDragging'); - if (draggingBlock) { - this.workspace.getCanvas().insertBefore(svgRoot, draggingBlock); + const draggingParentElement = draggingBlockElement?.parentElement as + | SVGElement + | null + | undefined; + const canvas = this.workspace.getCanvas(); + if (draggingParentElement === canvas) { + canvas.insertBefore(svgRoot, draggingBlockElement); } else { - this.workspace.getCanvas().appendChild(svgRoot); + canvas.appendChild(svgRoot); } this.translate(oldXY.x, oldXY.y); } diff --git a/core/dragging/block_drag_strategy.ts b/core/dragging/block_drag_strategy.ts index b53c131653b..76020f90b5b 100644 --- a/core/dragging/block_drag_strategy.ts +++ b/core/dragging/block_drag_strategy.ts @@ -238,7 +238,7 @@ export class BlockDragStrategy implements IDragStrategy { const currCandidate = this.connectionCandidate; const newCandidate = this.getConnectionCandidate(draggingBlock, delta); if (!newCandidate) { - this.connectionPreviewer!.hidePreview(); + this.connectionPreviewer?.hidePreview(); this.connectionCandidate = null; return; } @@ -254,7 +254,7 @@ export class BlockDragStrategy implements IDragStrategy { local.type === ConnectionType.OUTPUT_VALUE || local.type === ConnectionType.PREVIOUS_STATEMENT; const neighbourIsConnectedToRealBlock = - neighbour.isConnected() && !neighbour.targetBlock()!.isInsertionMarker(); + neighbour.isConnected() && !neighbour.targetBlock()?.isInsertionMarker(); if ( localIsOutputOrPrevious && neighbourIsConnectedToRealBlock && @@ -264,14 +264,14 @@ export class BlockDragStrategy implements IDragStrategy { local.type, ) ) { - this.connectionPreviewer!.previewReplacement( + this.connectionPreviewer?.previewReplacement( local, neighbour, neighbour.targetBlock()!, ); return; } - this.connectionPreviewer!.previewConnection(local, neighbour); + this.connectionPreviewer?.previewConnection(local, neighbour); } /** @@ -385,7 +385,7 @@ export class BlockDragStrategy implements IDragStrategy { dom.stopTextWidthCache(); blockAnimation.disconnectUiStop(); - this.connectionPreviewer!.hidePreview(); + this.connectionPreviewer?.hidePreview(); if (!this.block.isDeadOrDying() && this.dragging) { // These are expensive and don't need to be done if we're deleting, or @@ -413,7 +413,7 @@ export class BlockDragStrategy implements IDragStrategy { // Must dispose after connections are applied to not break the dynamic // connections plugin. See #7859 - this.connectionPreviewer!.dispose(); + this.connectionPreviewer?.dispose(); this.workspace.setResizesEnabled(true); eventUtils.setGroup(newGroup); } @@ -445,6 +445,9 @@ export class BlockDragStrategy implements IDragStrategy { return; } + this.connectionPreviewer?.hidePreview(); + this.connectionCandidate = null; + this.startChildConn?.connect(this.block.nextConnection); if (this.startParentConn) { switch (this.startParentConn.type) { @@ -471,9 +474,6 @@ export class BlockDragStrategy implements IDragStrategy { this.startChildConn = null; this.startParentConn = null; - this.connectionPreviewer!.hidePreview(); - this.connectionCandidate = null; - this.block.setDragging(false); this.dragging = false; } diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index 56cfba54290..85a9f29181c 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -1105,6 +1105,18 @@ suite('Blocks', function () { ); this.textJoinBlock = this.printBlock.getInputTargetBlock('TEXT'); this.textBlock = this.textJoinBlock.getInputTargetBlock('ADD0'); + this.extraTopBlock = Blockly.Xml.domToBlock( + Blockly.utils.xml.textToDom(` + + + + drag me + + + `), + this.workspace, + ); + this.extraNestedBlock = this.extraTopBlock.getInputTargetBlock('TEXT'); }); function assertBlockIsOnlyChild(parent, child, inputName) { @@ -1116,6 +1128,10 @@ suite('Blocks', function () { assert.equal(nonParent.getChildren().length, 0); assert.isNull(nonParent.getInputTargetBlock('TEXT')); assert.isNull(orphan.getParent()); + assert.equal( + orphan.getSvgRoot().parentElement, + orphan.workspace.getCanvas(), + ); } function assertOriginalSetup() { assertBlockIsOnlyChild(this.printBlock, this.textJoinBlock, 'TEXT'); @@ -1187,6 +1203,27 @@ suite('Blocks', function () { ); assertNonParentAndOrphan(this.textJoinBlock, this.textBlock, 'ADD0'); }); + test('Setting parent to null with dragging block', function () { + this.extraTopBlock.setDragging(true); + this.textBlock.outputConnection.disconnect(); + assert.doesNotThrow( + this.textBlock.setParent.bind(this.textBlock, null), + ); + assertNonParentAndOrphan(this.textJoinBlock, this.textBlock, 'ADD0'); + assert.equal( + this.textBlock.getSvgRoot().nextSibling, + this.extraTopBlock.getSvgRoot(), + ); + }); + test('Setting parent to null with non-top dragging block', function () { + this.extraNestedBlock.setDragging(true); + this.textBlock.outputConnection.disconnect(); + assert.doesNotThrow( + this.textBlock.setParent.bind(this.textBlock, null), + ); + assertNonParentAndOrphan(this.textJoinBlock, this.textBlock, 'ADD0'); + assert.equal(this.textBlock.getSvgRoot().nextSibling, null); + }); test('Setting parent to null without disconnecting', function () { assert.throws(this.textBlock.setParent.bind(this.textBlock, null)); assertOriginalSetup.call(this); From b8c2b731b7afa4f70620bb50387518c69f2bf014 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 30 Apr 2025 10:51:49 -0700 Subject: [PATCH 166/222] chore: update assignment workflow permissions for v12 (#8947) _This is a cherry-pick of #8811 into the v12 release branch._ ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) (ish--largely relying on the original testing for this PR since it's actions-related) ## The details ### Resolves This fixes the same issue as #8811 but on the v12 branch. ### Proposed Changes See #8811. ### Reason for Changes The failing assignee workflow will continue on v12 so the cherry-pick makes the next couple of PRs for this branch a bit nicer, but it's not a strong must-have since we'll eventually merge `develop` into `v12` which would then include #8811. ### Test Coverage N/A -- There's no strong benefit from automated tests for this workflow, and it was manually tested as part of #8811 (see https://github.com/google/blockly/pull/8811#issuecomment-2822741434). ### Documentation N/A -- No documentation changes are needed for this. ### Additional Information None. --- .github/workflows/assign_reviewers.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/assign_reviewers.yml b/.github/workflows/assign_reviewers.yml index 33bd9e778a9..850c7c805ff 100644 --- a/.github/workflows/assign_reviewers.yml +++ b/.github/workflows/assign_reviewers.yml @@ -15,6 +15,8 @@ on: jobs: requested-reviewer: runs-on: ubuntu-latest + permissions: + pull-requests: write steps: - name: Assign requested reviewer uses: actions/github-script@v7 From d82983f2c6536795875336bd6e3856433fbb2567 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 30 Apr 2025 15:43:41 -0700 Subject: [PATCH 167/222] feat: Make WorkspaceSvg and BlockSvg focusable (roll forward) (#8938) _Note: This is a roll forward of #8916 that was reverted in #8933. See Additional Information below._ ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #8913 Fixes #8914 Fixes part of #8771 ### Proposed Changes This updates `WorkspaceSvg` and `BlockSvg` to be focusable, that is, it makes the workspace a `IFocusableTree` and blocks `IFocusableNode`s. Some important details: - While this introduces focusable tree support for `Workspace` it doesn't include two other components that are obviously needed by the keyboard navigation plugin's playground: fields and connections. These will be introduced in subsequent PRs. - Blocks are set up to automatically synchronize their selection state with their focus state. This will eventually help to replace `LineCursor`'s responsibility for managing selection state itself. - The tabindex property for the workspace and its ARIA label have been moved down to the `.blocklyWorkspace` element itself rather than its wrapper. This helps address some tab stop issues that are already addressed in the plugin (via monkey patches), but also to ensure that the workspace's main SVG group interacts correctly with `FocusManager`. - `WorkspaceSvg` is being initially set up to default to its first top block when being focused for the first time. This is to match parity with the keyboard navigation plugin, however the latter also has functionality for defaulting to a position when no blocks are present. It's not clear how to actually support this under the new focus-based system (without adding an ephemeral element on which to focus), or if it's even necessary (since the workspace root can hold focus). - `css.ts` was updated to remove `blocklyActiveFocus` and `blocklyPassiveFocus` since these have unintended highlighting consequences that aren't actually desirable yet. Instead, the exact styling for active/passive focus will be iterated in the keyboard navigation plugin project and moved to Core once finalized. ### Reason for Changes This is part of an ongoing effort to ensure key components of Blockly are focusable so that they can be keyboard-navigable (with other needed changes yet both in Core Blockly and the keyboard navigation plugin). ### Test Coverage No new tests have been added. It's certainly possible to add unit tests for the focusable configurations being introduced in this PR, but it may not be highly beneficial. It's largely assumed that the individual implementations should work due to a highly tested FocusManager, and it may be the case that the interactions of the components working together is far more important to verify (that is, the end user flows). The latter is planned to be tackled as part of #8915. ### Documentation No documentation changes should be needed here. ### Additional Information This includes changes that have been pulled from #8875. This was originally merged in #8916 but was reverted in #8933 due to https://github.com/google/blockly-keyboard-experimentation/issues/481. This actually contains no differences from the original PR except for `css.ts` which are documented above. It does employ a new merge strategy: all of the necessary PRs to move both Core and the plugin over to using `FocusManager` will be staged and merged in quick succession as ensuring the plugin works for each constituent change (vs. the final one) is quite complex. Thus, this PR *does* break the plugin, and won't be merged until its subsequent PRs are approved and also ready for merging. Edit: See https://github.com/google/blockly/pull/8938#issuecomment-2843589525 for why this actually is being merged a bit sooner than originally planned. Keeping the original reasoning above for context. --- core/block_svg.ts | 28 +++++++++- core/css.ts | 12 +---- core/inject.ts | 5 -- core/renderers/common/path_object.ts | 2 +- core/workspace_svg.ts | 77 +++++++++++++++++++++++++++- 5 files changed, 105 insertions(+), 19 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 4cef79df8e6..04b7d88d1f2 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -44,6 +44,8 @@ import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {ICopyable} from './interfaces/i_copyable.js'; import {IDeletable} from './interfaces/i_deletable.js'; import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {IIcon} from './interfaces/i_icon.js'; import * as internalConstants from './internal_constants.js'; import {MarkerManager} from './marker_manager.js'; @@ -76,7 +78,8 @@ export class BlockSvg IContextMenu, ICopyable, IDraggable, - IDeletable + IDeletable, + IFocusableNode { /** * Constant for identifying rows that are to be rendered inline. @@ -210,6 +213,7 @@ export class BlockSvg // Expose this block's ID on its top-level SVG group. this.svgGroup.setAttribute('data-id', this.id); + svgPath.id = this.id; this.doInit_(); } @@ -1827,4 +1831,26 @@ export class BlockSvg ); } } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + return this.pathObject.svgPath; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.workspace; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void { + common.setSelected(this); + } + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void { + if (common.getSelected() === this) { + common.setSelected(null); + } + } } diff --git a/core/css.ts b/core/css.ts index 6ca262f3b25..ed47e8037ec 100644 --- a/core/css.ts +++ b/core/css.ts @@ -463,8 +463,8 @@ input[type=number] { } .blocklyMenuSeparator { - background-color: #ccc; - height: 1px; + background-color: #ccc; + height: 1px; border: 0; margin-left: 4px; margin-right: 4px; @@ -494,12 +494,4 @@ input[type=number] { cursor: grabbing; } -.blocklyActiveFocus { - outline-color: #2ae; - outline-width: 2px; -} -.blocklyPassiveFocus { - outline-color: #3fdfff; - outline-width: 1.5px; -} `; diff --git a/core/inject.ts b/core/inject.ts index de78fbfae75..34d9c1795f8 100644 --- a/core/inject.ts +++ b/core/inject.ts @@ -13,13 +13,11 @@ import * as common from './common.js'; import * as Css from './css.js'; import * as dropDownDiv from './dropdowndiv.js'; import {Grid} from './grid.js'; -import {Msg} from './msg.js'; import {Options} from './options.js'; import {ScrollbarPair} from './scrollbar_pair.js'; import {ShortcutRegistry} from './shortcut_registry.js'; import * as Tooltip from './tooltip.js'; import * as Touch from './touch.js'; -import * as aria from './utils/aria.js'; import * as dom from './utils/dom.js'; import {Svg} from './utils/svg.js'; import * as WidgetDiv from './widgetdiv.js'; @@ -56,8 +54,6 @@ export function inject( if (opt_options?.rtl) { dom.addClass(subContainer, 'blocklyRTL'); } - subContainer.tabIndex = 0; - aria.setState(subContainer, aria.State.LABEL, Msg['WORKSPACE_ARIA_LABEL']); containerElement!.appendChild(subContainer); const svg = createDom(subContainer, options); @@ -126,7 +122,6 @@ function createDom(container: HTMLElement, options: Options): SVGElement { 'xmlns:xlink': dom.XLINK_NS, 'version': '1.1', 'class': 'blocklySvg', - 'tabindex': '0', }, container, ); diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index 077f80bb741..72cf2a594ce 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -62,7 +62,7 @@ export class PathObject implements IPathObject { /** The primary path of the block. */ this.svgPath = dom.createSvgElement( Svg.PATH, - {'class': 'blocklyPath'}, + {'class': 'blocklyPath', 'tabindex': '-1'}, this.svgRoot, ); diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index e8d411651c5..2648005b257 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -37,6 +37,7 @@ import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import {Flyout} from './flyout_base.js'; import type {FlyoutButton} from './flyout_button.js'; +import {getFocusManager} from './focus_manager.js'; import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; @@ -44,6 +45,8 @@ import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {IDragTarget} from './interfaces/i_drag_target.js'; import type {IFlyout} from './interfaces/i_flyout.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; import type {IToolbox} from './interfaces/i_toolbox.js'; import type { @@ -54,6 +57,7 @@ import type {LineCursor} from './keyboard_nav/line_cursor.js'; import type {Marker} from './keyboard_nav/marker.js'; import {LayerManager} from './layer_manager.js'; import {MarkerManager} from './marker_manager.js'; +import {Msg} from './msg.js'; import {Options} from './options.js'; import * as Procedures from './procedures.js'; import * as registry from './registry.js'; @@ -66,6 +70,7 @@ import {Classic} from './theme/classic.js'; import {ThemeManager} from './theme_manager.js'; import * as Tooltip from './tooltip.js'; import type {Trashcan} from './trashcan.js'; +import * as aria from './utils/aria.js'; import * as arrayUtils from './utils/array.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; @@ -93,7 +98,7 @@ const ZOOM_TO_FIT_MARGIN = 20; */ export class WorkspaceSvg extends Workspace - implements IASTNodeLocationSvg, IContextMenu + implements IASTNodeLocationSvg, IContextMenu, IFocusableNode, IFocusableTree { /** * A wrapper function called when a resize event occurs. @@ -764,7 +769,19 @@ export class WorkspaceSvg * * */ - this.svgGroup_ = dom.createSvgElement(Svg.G, {'class': 'blocklyWorkspace'}); + this.svgGroup_ = dom.createSvgElement(Svg.G, { + 'class': 'blocklyWorkspace', + // Only the top-level workspace should be tabbable. + 'tabindex': injectionDiv ? '0' : '-1', + 'id': this.id, + }); + if (injectionDiv) { + aria.setState( + this.svgGroup_, + aria.State.LABEL, + Msg['WORKSPACE_ARIA_LABEL'], + ); + } // Note that a alone does not receive mouse events--it must have a // valid target inside it. If no background class is specified, as in the @@ -840,6 +857,9 @@ export class WorkspaceSvg this.getTheme(), isParentWorkspace ? this.getInjectionDiv() : undefined, ); + + getFocusManager().registerTree(this); + return this.svgGroup_; } @@ -924,6 +944,10 @@ export class WorkspaceSvg document.body.removeEventListener('wheel', this.dummyWheelListener); this.dummyWheelListener = null; } + + if (getFocusManager().isRegistered(this)) { + getFocusManager().unregisterTree(this); + } } /** @@ -2631,6 +2655,55 @@ export class WorkspaceSvg deltaY *= scale; this.scroll(this.scrollX + deltaX, this.scrollY + deltaY); } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + return this.svgGroup_; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableTree.getRootFocusableNode. */ + getRootFocusableNode(): IFocusableNode { + return this; + } + + /** See IFocusableTree.getRestoredFocusableNode. */ + getRestoredFocusableNode( + previousNode: IFocusableNode | null, + ): IFocusableNode | null { + if (!previousNode) { + return this.getTopBlocks(true)[0] ?? null; + } else return null; + } + + /** See IFocusableTree.getNestedTrees. */ + getNestedTrees(): Array { + return []; + } + + /** See IFocusableTree.lookUpFocusableNode. */ + lookUpFocusableNode(id: string): IFocusableNode | null { + return this.getBlockById(id) as IFocusableNode; + } + + /** See IFocusableTree.onTreeFocus. */ + onTreeFocus( + _node: IFocusableNode, + _previousTree: IFocusableTree | null, + ): void {} + + /** See IFocusableTree.onTreeBlur. */ + onTreeBlur(_nextTree: IFocusableTree | null): void {} } /** From cac8f0116ce6c724c8062110e9cad02b8db64887 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 30 Apr 2025 15:49:29 -0700 Subject: [PATCH 168/222] feat: Make toolbox and flyout focusable (roll forward) (#8939) _Note: This is a roll forward of #8920 that was reverted in #8933. See Additional Information below._ ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #8918 Fixes #8919 Fixes part of #8943 Fixes part of #8771 ### Proposed Changes This updates several classes in order to make toolboxes and flyouts focusable: - `IFlyout` is now an `IFocusableTree` with corresponding implementations in `FlyoutBase`. - `IToolbox` is now an `IFocusableTree` with corresponding implementations in `Toolbox`. - `IToolboxItem` is now an `IFocusableNode` with corresponding implementations in `ToolboxItem`. - As the primary toolbox items, `ToolboxCategory` and `ToolboxSeparator` were updated to have -1 tab indexes and defined IDs to help `ToolboxItem` fulfill its contracted for `IFocusableNode.getFocusableElement`. - `FlyoutButton` is now an `IFocusableNode` (with corresponding ID generation, tab index setting, and ID matching for retrieval in `WorkspaceSvg`). Each of these two new focusable trees have specific noteworthy behaviors behaviors: - `Toolbox` will automatically indicate that its first item should be focused (if one is present), even overriding the ability to focus the toolbox's root (however there are some cases where that can still happen). - `Toolbox` will automatically synchronize its selection state with its item nodes being focused. - `FlyoutBase`, now being a focusable tree, has had a tab index of 0 added. Normally a tab index of -1 is all that's needed, but the keyboard navigation plugin specifically uses 0 for flyout so that the flyout is tabbable. This is a **new** tab stop being introduced. - `FlyoutBase` holds a workspace (for rendering blocks) and, since `WorkspaceSvg` is already set up to be a focusable tree, it's represented as a subtree to `FlyoutBase`. This does introduce some wonky behaviors: the flyout's root will have passive focus while its contents have active focus. This could be manually disabled with some CSS if it ends up being a confusing user experience. - Both `FlyoutBase` and `WorkspaceSvg` have built-in behaviors for detecting when a user tries navigating away from an open flyout to ensure that the flyout is closed when it's supposed to be. That is, the flyout is auto-hideable and a non-flyout, non-toolbox node has then been focused. This matches parity with the `T`/`Esc` flows supported in the keyboard navigation plugin playground. One other thing to note: `Toolbox` had a few tests to update that were trying to reinit a toolbox without first disposing of it (which was caught by one of `FocusManager`'s state guardrails). This only addresses part of #8943: it adds support for `FlyoutButton` which covers both buttons and labels. However, a longer-term solution may be to change `FlyoutItem` itself to force using an `IFocusableNode` as its element. ### Reason for Changes This is part of an ongoing effort to ensure key components of Blockly are focusable so that they can be keyboard-navigable (with other needed changes yet both in Core Blockly and the keyboard navigation plugin). ### Test Coverage No new tests have been added. It's certainly possible to add unit tests for the focusable configurations being introduced in this PR, but it may not be highly beneficial. It's largely assumed that the individual implementations should work due to a highly tested FocusManager, and it may be the case that the interactions of the components working together is far more important to verify (that is, the end user flows). The latter is planned to be tackled as part of #8915. ### Documentation No documentation changes should be needed here. ### Additional Information This includes changes that have been pulled from #8875. This was originally merged in #8916 but was reverted in #8933 due to https://github.com/google/blockly-keyboard-experimentation/issues/481. Note that this does contain a number of differences from the original PR (namely, changes in `WorkspaceSvg` and `FlyoutButton` in order to make `FlyoutButton`s focusable). Otherwise, this has the same caveats as those noted in #8938 with regards to the experimental keyboard navigation plugin. --- core/flyout_base.ts | 69 ++++++++++++++++++++++++++- core/flyout_button.ts | 31 ++++++++++++- core/interfaces/i_flyout.ts | 3 +- core/interfaces/i_toolbox.ts | 3 +- core/interfaces/i_toolbox_item.ts | 4 +- core/toolbox/category.ts | 2 + core/toolbox/separator.ts | 2 + core/toolbox/toolbox.ts | 77 ++++++++++++++++++++++++++++++- core/toolbox/toolbox_item.ts | 24 ++++++++++ core/workspace_svg.ts | 32 ++++++++++++- tests/mocha/toolbox_test.js | 3 ++ 11 files changed, 240 insertions(+), 10 deletions(-) diff --git a/core/flyout_base.ts b/core/flyout_base.ts index e738470a606..a0ba75eb9fe 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -21,9 +21,12 @@ import * as eventUtils from './events/utils.js'; import {FlyoutItem} from './flyout_item.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; +import {getFocusManager} from './focus_manager.js'; import {IAutoHideable} from './interfaces/i_autohideable.js'; import type {IFlyout} from './interfaces/i_flyout.js'; import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import {IFocusableNode} from './interfaces/i_focusable_node.js'; +import {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import * as renderManagement from './render_management.js'; @@ -43,7 +46,7 @@ import {WorkspaceSvg} from './workspace_svg.js'; */ export abstract class Flyout extends DeleteArea - implements IAutoHideable, IFlyout + implements IAutoHideable, IFlyout, IFocusableNode { /** * Position the flyout. @@ -303,6 +306,7 @@ export abstract class Flyout // hide/show code will set up proper visibility and size later. this.svgGroup_ = dom.createSvgElement(tagName, { 'class': 'blocklyFlyout', + 'tabindex': '0', }); this.svgGroup_.style.display = 'none'; this.svgBackground_ = dom.createSvgElement( @@ -317,6 +321,9 @@ export abstract class Flyout this.workspace_ .getThemeManager() .subscribe(this.svgBackground_, 'flyoutOpacity', 'fill-opacity'); + + getFocusManager().registerTree(this); + return this.svgGroup_; } @@ -398,6 +405,7 @@ export abstract class Flyout if (this.svgGroup_) { dom.removeNode(this.svgGroup_); } + getFocusManager().unregisterTree(this); } /** @@ -961,4 +969,63 @@ export abstract class Flyout return null; } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + if (!this.svgGroup_) throw new Error('Flyout DOM is not yet created.'); + return this.svgGroup_; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableTree.getRootFocusableNode. */ + getRootFocusableNode(): IFocusableNode { + return this; + } + + /** See IFocusableTree.getRestoredFocusableNode. */ + getRestoredFocusableNode( + _previousNode: IFocusableNode | null, + ): IFocusableNode | null { + return null; + } + + /** See IFocusableTree.getNestedTrees. */ + getNestedTrees(): Array { + return [this.workspace_]; + } + + /** See IFocusableTree.lookUpFocusableNode. */ + lookUpFocusableNode(_id: string): IFocusableNode | null { + // No focusable node needs to be returned since the flyout's subtree is a + // workspace that will manage its own focusable state. + return null; + } + + /** See IFocusableTree.onTreeFocus. */ + onTreeFocus( + _node: IFocusableNode, + _previousTree: IFocusableTree | null, + ): void {} + + /** See IFocusableTree.onTreeBlur. */ + onTreeBlur(nextTree: IFocusableTree | null): void { + const toolbox = this.targetWorkspace.getToolbox(); + // If focus is moving to either the toolbox or the flyout's workspace, do + // not close the flyout. For anything else, do close it since the flyout is + // no longer focused. + if (toolbox && nextTree === toolbox) return; + if (nextTree === this.workspace_) return; + if (toolbox) toolbox.clearSelection(); + this.autoHide(false); + } } diff --git a/core/flyout_button.ts b/core/flyout_button.ts index 3b9b2fe0735..666a2e081bf 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -15,7 +15,10 @@ import type {IASTNodeLocationSvg} from './blockly.js'; import * as browserEvents from './browser_events.js'; import * as Css from './css.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IRenderedElement} from './interfaces/i_rendered_element.js'; +import {idGenerator} from './utils.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import * as parsing from './utils/parsing.js'; @@ -29,7 +32,11 @@ import type {WorkspaceSvg} from './workspace_svg.js'; * Class for a button or label in the flyout. */ export class FlyoutButton - implements IASTNodeLocationSvg, IBoundedElement, IRenderedElement + implements + IASTNodeLocationSvg, + IBoundedElement, + IRenderedElement, + IFocusableNode { /** The horizontal margin around the text in the button. */ static TEXT_MARGIN_X = 5; @@ -68,6 +75,9 @@ export class FlyoutButton */ cursorSvg: SVGElement | null = null; + /** The unique ID for this FlyoutButton. */ + private id: string; + /** * @param workspace The workspace in which to place this button. * @param targetWorkspace The flyout's target workspace. @@ -105,9 +115,10 @@ export class FlyoutButton cssClass += ' ' + this.cssClass; } + this.id = idGenerator.getNextUniqueId(); this.svgGroup = dom.createSvgElement( Svg.G, - {'class': cssClass}, + {'id': this.id, 'class': cssClass, 'tabindex': '-1'}, this.workspace.getCanvas(), ); @@ -389,6 +400,22 @@ export class FlyoutButton getSvgRoot() { return this.svgGroup; } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + return this.svgGroup; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.workspace; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} } /** CSS for buttons and labels. See css.js for use. */ diff --git a/core/interfaces/i_flyout.ts b/core/interfaces/i_flyout.ts index 42204775ece..067cd5ef20d 100644 --- a/core/interfaces/i_flyout.ts +++ b/core/interfaces/i_flyout.ts @@ -12,12 +12,13 @@ import type {Coordinate} from '../utils/coordinate.js'; import type {Svg} from '../utils/svg.js'; import type {FlyoutDefinition} from '../utils/toolbox.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; +import {IFocusableTree} from './i_focusable_tree.js'; import type {IRegistrable} from './i_registrable.js'; /** * Interface for a flyout. */ -export interface IFlyout extends IRegistrable { +export interface IFlyout extends IRegistrable, IFocusableTree { /** Whether the flyout is laid out horizontally or not. */ horizontalLayout: boolean; diff --git a/core/interfaces/i_toolbox.ts b/core/interfaces/i_toolbox.ts index 1780af94d8a..f5d9c9fd7c6 100644 --- a/core/interfaces/i_toolbox.ts +++ b/core/interfaces/i_toolbox.ts @@ -9,13 +9,14 @@ import type {ToolboxInfo} from '../utils/toolbox.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; import type {IFlyout} from './i_flyout.js'; +import type {IFocusableTree} from './i_focusable_tree.js'; import type {IRegistrable} from './i_registrable.js'; import type {IToolboxItem} from './i_toolbox_item.js'; /** * Interface for a toolbox. */ -export interface IToolbox extends IRegistrable { +export interface IToolbox extends IRegistrable, IFocusableTree { /** Initializes the toolbox. */ init(): void; diff --git a/core/interfaces/i_toolbox_item.ts b/core/interfaces/i_toolbox_item.ts index e3c9864f0c0..661624fd7e8 100644 --- a/core/interfaces/i_toolbox_item.ts +++ b/core/interfaces/i_toolbox_item.ts @@ -6,10 +6,12 @@ // Former goog.module ID: Blockly.IToolboxItem +import type {IFocusableNode} from './i_focusable_node.js'; + /** * Interface for an item in the toolbox. */ -export interface IToolboxItem { +export interface IToolboxItem extends IFocusableNode { /** * Initializes the toolbox item. * This includes creating the DOM and updating the state of any items based diff --git a/core/toolbox/category.ts b/core/toolbox/category.ts index d8ee8736ea6..fc7d1aa03cf 100644 --- a/core/toolbox/category.ts +++ b/core/toolbox/category.ts @@ -225,6 +225,8 @@ export class ToolboxCategory */ protected createContainer_(): HTMLDivElement { const container = document.createElement('div'); + container.tabIndex = -1; + container.id = this.getId(); const className = this.cssConfig_['container']; if (className) { dom.addClass(container, className); diff --git a/core/toolbox/separator.ts b/core/toolbox/separator.ts index 31ccb7e42f3..44ae358cf53 100644 --- a/core/toolbox/separator.ts +++ b/core/toolbox/separator.ts @@ -54,6 +54,8 @@ export class ToolboxSeparator extends ToolboxItem { */ protected createDom_(): HTMLDivElement { const container = document.createElement('div'); + container.tabIndex = -1; + container.id = this.getId(); const className = this.cssConfig_['container']; if (className) { dom.addClass(container, className); diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index b0fd82e97f2..ceb756afbd6 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -22,11 +22,14 @@ import {DeleteArea} from '../delete_area.js'; import '../events/events_toolbox_item_select.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; +import {getFocusManager} from '../focus_manager.js'; import type {IAutoHideable} from '../interfaces/i_autohideable.js'; import type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js'; import {isDeletable} from '../interfaces/i_deletable.js'; import type {IDraggable} from '../interfaces/i_draggable.js'; import type {IFlyout} from '../interfaces/i_flyout.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import type {IKeyboardAccessible} from '../interfaces/i_keyboard_accessible.js'; import type {ISelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.js'; import {isSelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.js'; @@ -51,7 +54,12 @@ import {CollapsibleToolboxCategory} from './collapsible_category.js'; */ export class Toolbox extends DeleteArea - implements IAutoHideable, IKeyboardAccessible, IStyleable, IToolbox + implements + IAutoHideable, + IKeyboardAccessible, + IStyleable, + IToolbox, + IFocusableNode { /** * The unique ID for this component that is used to register with the @@ -163,6 +171,7 @@ export class Toolbox ComponentManager.Capability.DRAG_TARGET, ], }); + getFocusManager().registerTree(this); } /** @@ -177,7 +186,6 @@ export class Toolbox const container = this.createContainer_(); this.contentsDiv_ = this.createContentsContainer_(); - this.contentsDiv_.tabIndex = 0; aria.setRole(this.contentsDiv_, aria.Role.TREE); container.appendChild(this.contentsDiv_); @@ -194,6 +202,7 @@ export class Toolbox */ protected createContainer_(): HTMLDivElement { const toolboxContainer = document.createElement('div'); + toolboxContainer.tabIndex = 0; toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v'); dom.addClass(toolboxContainer, 'blocklyToolbox'); toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); @@ -1077,7 +1086,71 @@ export class Toolbox this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv); dom.removeNode(this.HtmlDiv); } + + getFocusManager().unregisterTree(this); + } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + if (!this.HtmlDiv) throw Error('Toolbox DOM has not yet been created.'); + return this.HtmlDiv; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableTree.getRootFocusableNode. */ + getRootFocusableNode(): IFocusableNode { + return this; + } + + /** See IFocusableTree.getRestoredFocusableNode. */ + getRestoredFocusableNode( + previousNode: IFocusableNode | null, + ): IFocusableNode | null { + // Always try to select the first selectable toolbox item rather than the + // root of the toolbox. + if (!previousNode || previousNode === this) { + return this.getToolboxItems().find((item) => item.isSelectable()) ?? null; + } + return null; } + + /** See IFocusableTree.getNestedTrees. */ + getNestedTrees(): Array { + return []; + } + + /** See IFocusableTree.lookUpFocusableNode. */ + lookUpFocusableNode(id: string): IFocusableNode | null { + return this.getToolboxItemById(id) as IFocusableNode; + } + + /** See IFocusableTree.onTreeFocus. */ + onTreeFocus( + node: IFocusableNode, + _previousTree: IFocusableTree | null, + ): void { + if (node !== this) { + // Only select the item if it isn't already selected so as to not toggle. + if (this.getSelectedItem() !== node) { + this.setSelectedItem(node as IToolboxItem); + } + } else { + this.clearSelection(); + } + } + + /** See IFocusableTree.onTreeBlur. */ + onTreeBlur(_nextTree: IFocusableTree | null): void {} } /** CSS for Toolbox. See css.js for use. */ diff --git a/core/toolbox/toolbox_item.ts b/core/toolbox/toolbox_item.ts index ef9d979ab43..0d46a5eadfd 100644 --- a/core/toolbox/toolbox_item.ts +++ b/core/toolbox/toolbox_item.ts @@ -12,6 +12,7 @@ // Former goog.module ID: Blockly.ToolboxItem import type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import type {IToolbox} from '../interfaces/i_toolbox.js'; import type {IToolboxItem} from '../interfaces/i_toolbox_item.js'; import * as idGenerator from '../utils/idgenerator.js'; @@ -148,5 +149,28 @@ export class ToolboxItem implements IToolboxItem { * @param _isVisible True if category should be visible. */ setVisible_(_isVisible: boolean) {} + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + const div = this.getDiv(); + if (!div) { + throw Error('Trying to access toolbox item before DOM is initialized.'); + } + if (!(div instanceof HTMLElement)) { + throw Error('Toolbox item div is unexpectedly not an HTML element.'); + } + return div as HTMLElement; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.parentToolbox_; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} } // nop by default diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 2648005b257..921fb72ecc7 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -45,7 +45,10 @@ import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {IDragTarget} from './interfaces/i_drag_target.js'; import type {IFlyout} from './interfaces/i_flyout.js'; -import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import { + isFocusableNode, + type IFocusableNode, +} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; import type {IToolbox} from './interfaces/i_toolbox.js'; @@ -2693,6 +2696,19 @@ export class WorkspaceSvg /** See IFocusableTree.lookUpFocusableNode. */ lookUpFocusableNode(id: string): IFocusableNode | null { + // Check against flyout items if this workspace is part of a flyout. Note + // that blocks may match against this pass before reaching getBlockById() + // below (but only for a flyout workspace). + const flyout = this.targetWorkspace?.getFlyout(); + if (this.isFlyout && flyout) { + for (const flyoutItem of flyout.getContents()) { + const elem = flyoutItem.getElement(); + if (isFocusableNode(elem) && elem.getFocusableElement().id === id) { + return elem; + } + } + } + return this.getBlockById(id) as IFocusableNode; } @@ -2703,7 +2719,19 @@ export class WorkspaceSvg ): void {} /** See IFocusableTree.onTreeBlur. */ - onTreeBlur(_nextTree: IFocusableTree | null): void {} + onTreeBlur(nextTree: IFocusableTree | null): void { + // If the flyout loses focus, make sure to close it. + if (this.isFlyout && this.targetWorkspace) { + // Only hide the flyout if the flyout's workspace is losing focus and that + // focus isn't returning to the flyout itself or the toolbox. + const flyout = this.targetWorkspace.getFlyout(); + const toolbox = this.targetWorkspace.getToolbox(); + if (flyout && nextTree === flyout) return; + if (toolbox && nextTree === toolbox) return; + if (toolbox) toolbox.clearSelection(); + if (flyout && flyout instanceof Flyout) flyout.autoHide(false); + } + } } /** diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js index 10bfd335223..f32319c6779 100644 --- a/tests/mocha/toolbox_test.js +++ b/tests/mocha/toolbox_test.js @@ -54,6 +54,7 @@ suite('Toolbox', function () { const themeManagerSpy = sinon.spy(themeManager, 'subscribe'); const componentManager = this.toolbox.workspace_.getComponentManager(); sinon.stub(componentManager, 'addComponent'); + this.toolbox.dispose(); // Dispose of the old toolbox so that it can be reinited. this.toolbox.init(); sinon.assert.calledWith( themeManagerSpy, @@ -72,12 +73,14 @@ suite('Toolbox', function () { const renderSpy = sinon.spy(this.toolbox, 'render'); const componentManager = this.toolbox.workspace_.getComponentManager(); sinon.stub(componentManager, 'addComponent'); + this.toolbox.dispose(); // Dispose of the old toolbox so that it can be reinited. this.toolbox.init(); sinon.assert.calledOnce(renderSpy); }); test('Init called -> Flyout is initialized', function () { const componentManager = this.toolbox.workspace_.getComponentManager(); sinon.stub(componentManager, 'addComponent'); + this.toolbox.dispose(); // Dispose of the old toolbox so that it can be reinited. this.toolbox.init(); assert.isDefined(this.toolbox.getFlyout()); }); From f68081bf60dfb98742a06bbab9c47a83d0f496a6 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 30 Apr 2025 15:54:21 -0700 Subject: [PATCH 169/222] feat: Make fields focusable (#8923) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #8922 Fixes #8929 Fixes part of #8771 ### Proposed Changes This PR introduces support for fields to be focusable (and thus navigable with keyboard navigation when paired with downstream changes to `LineCursor` and the keyboard navigation plugin). This is a largely isolated change in how it fundamentally works: - `Field` was updated to become an `IFocusableNode`. Note that it uses a specific string-based ID schema in order to ensure that it can be properly linked back to its unique block (which helps make the search for the field in `WorkspaceSvg` a bit more efficient). This could be done with a globally unique ID, instead, but all fields would need to be searched vs. just those for the field's parent block. - The drop-down and widget divs have been updated to manage ephemeral focus with `FocusManager` when they're open for non-system dialogs (ephemeral focus isn't needed for system dialogs/prompts since those already take/restore focus in a native way that `FocusManager` will respond to--this may require future work, however, if the restoration causes unexpected behavior for users). This approach was done due to a suggestion from @maribethb as the alternative would be a more complicated breaking change (forcing `Field` subclasses to properly manage ephemeral focus). It may still be the case that certain cases will need to do so, but widget and drop-down divs seem to address the majority of possibilities. **Important**: `Input`s are not explicitly being supported here. As far as I can tell, we can't run into a case where `LineCursor` tries to set an input node, though perhaps I simply haven't come across this case. Supporting `Fields` and `Connections` (per #8928) seems to cover the main needed cases, though making `Input`s focusable may be a future requirement. ### Reason for Changes This is part of an ongoing effort to ensure key components of Blockly are focusable so that they can be keyboard-navigable (with other needed changes yet both in Core Blockly and the keyboard navigation plugin). Note that #8929 isn't broadly addressed since making widget & drop down divs manage ephemeral focus directly addresses a large class of cases. Additional cases may arise where a plugin or Blockly integration may require additional effort to make keyboard navigation work for their field--this may be best addressed with documentation and guidance. ### Test Coverage No new tests have been added. It's certainly possible to add unit tests for the focusable configurations being introduced in this PR, but it may not be highly beneficial. It's largely assumed that the individual implementations should work due to a highly tested FocusManager, and it may be the case that the interactions of the components working together is far more important to verify (that is, the end user flows). The latter is planned to be tackled as part of #8915. ### Documentation No new documentation is planned, however it may be prudent to update the field documentation in the future to explain how to utilize ephemeral focus when specifically building compatibility for keyboard navigation. ### Additional Information This includes changes that have been pulled from #8875. --- core/dialog.ts | 7 +++++++ core/dropdowndiv.ts | 14 ++++++++++++++ core/field.ts | 42 +++++++++++++++++++++++++++++++++++++++--- core/widgetdiv.ts | 15 +++++++++++++-- core/workspace_svg.ts | 12 ++++++++++++ 5 files changed, 85 insertions(+), 5 deletions(-) diff --git a/core/dialog.ts b/core/dialog.ts index 374961323da..96631e9cbc7 100644 --- a/core/dialog.ts +++ b/core/dialog.ts @@ -33,6 +33,8 @@ const defaultPrompt = function ( defaultValue: string, callback: (result: string | null) => void, ) { + // NOTE TO DEVELOPER: Ephemeral focus doesn't need to be taken for the native + // window prompt since it prevents focus from changing while open. callback(window.prompt(message, defaultValue)); }; @@ -116,6 +118,11 @@ export function prompt( /** * Sets the function to be run when Blockly.dialog.prompt() is called. * + * **Important**: When overridding this, be aware that non-native prompt + * experiences may require managing ephemeral focus in FocusManager. This isn't + * needed for the native window prompt because it prevents focus from being + * changed while open. + * * @param promptFunction The function to be run, or undefined to restore the * default implementation. * @see Blockly.dialog.prompt diff --git a/core/dropdowndiv.ts b/core/dropdowndiv.ts index 0d259bc53d7..dcf8fa24ef7 100644 --- a/core/dropdowndiv.ts +++ b/core/dropdowndiv.ts @@ -15,6 +15,7 @@ import type {BlockSvg} from './block_svg.js'; import * as common from './common.js'; import type {Field} from './field.js'; +import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js'; import * as dom from './utils/dom.js'; import * as math from './utils/math.js'; import {Rect} from './utils/rect.js'; @@ -82,6 +83,9 @@ let owner: Field | null = null; /** Whether the dropdown was positioned to a field or the source block. */ let positionToField: boolean | null = null; +/** Callback to FocusManager to return ephemeral focus when the div closes. */ +let returnEphemeralFocus: ReturnEphemeralFocus | null = null; + /** * Dropdown bounds info object used to encapsulate sizing information about a * bounding element (bounding box and width/height). @@ -338,6 +342,8 @@ export function show( dom.addClass(div, renderedClassName); dom.addClass(div, themeClassName); + returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div); + // When we change `translate` multiple times in close succession, // Chrome may choose to wait and apply them all at once. // Since we want the translation to initial X, Y to be immediate, @@ -623,6 +629,10 @@ export function hide() { animateOutTimer = setTimeout(function () { hideWithoutAnimation(); }, ANIMATION_TIME * 1000); + if (returnEphemeralFocus) { + returnEphemeralFocus(); + returnEphemeralFocus = null; + } if (onHide) { onHide(); onHide = null; @@ -638,6 +648,10 @@ export function hideWithoutAnimation() { clearTimeout(animateOutTimer); } + if (returnEphemeralFocus) { + returnEphemeralFocus(); + returnEphemeralFocus = null; + } if (onHide) { onHide(); onHide = null; diff --git a/core/field.ts b/core/field.ts index 725a2867d9e..a5e43a27665 100644 --- a/core/field.ts +++ b/core/field.ts @@ -25,6 +25,8 @@ import * as eventUtils from './events/utils.js'; import type {Input} from './inputs/input.js'; import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js'; import type {IRegistrable} from './interfaces/i_registrable.js'; import {ISerializable} from './interfaces/i_serializable.js'; @@ -34,6 +36,7 @@ import type {KeyboardShortcut} from './shortcut_registry.js'; import * as Tooltip from './tooltip.js'; import type {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; +import * as idGenerator from './utils/idgenerator.js'; import * as parsing from './utils/parsing.js'; import {Rect} from './utils/rect.js'; import {Size} from './utils/size.js'; @@ -42,7 +45,7 @@ import {Svg} from './utils/svg.js'; import * as userAgent from './utils/useragent.js'; import * as utilsXml from './utils/xml.js'; import * as WidgetDiv from './widgetdiv.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; +import {WorkspaceSvg} from './workspace_svg.js'; /** * A function that is called to validate changes to the field's value before @@ -72,7 +75,8 @@ export abstract class Field IASTNodeLocationWithBlock, IKeyboardAccessible, IRegistrable, - ISerializable + ISerializable, + IFocusableNode { /** * To overwrite the default value which is set in **Field**, directly update @@ -191,6 +195,9 @@ export abstract class Field */ SERIALIZABLE = false; + /** The unique ID of this field. */ + private id_: string | null = null; + /** * @param value The initial value of the field. * Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by @@ -255,6 +262,7 @@ export abstract class Field throw Error('Field already bound to a block'); } this.sourceBlock_ = block; + this.id_ = `${block.id}_field_${idGenerator.getNextUniqueId()}`; } /** @@ -298,7 +306,12 @@ export abstract class Field // Field has already been initialized once. return; } - this.fieldGroup_ = dom.createSvgElement(Svg.G, {}); + const id = this.id_; + if (!id) throw new Error('Expected ID to be defined prior to init.'); + this.fieldGroup_ = dom.createSvgElement(Svg.G, { + 'tabindex': '-1', + 'id': id, + }); if (!this.isVisible()) { this.fieldGroup_.style.display = 'none'; } @@ -1401,6 +1414,29 @@ export abstract class Field } } + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + if (!this.fieldGroup_) { + throw Error('This field currently has no representative DOM element.'); + } + return this.fieldGroup_; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + const block = this.getSourceBlock(); + if (!block) { + throw new UnattachedFieldError(); + } + return block.workspace as WorkspaceSvg; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + /** * Subclasses should reimplement this method to construct their Field * subclass from a JSON arg object. diff --git a/core/widgetdiv.ts b/core/widgetdiv.ts index f167b6cf04d..cb006160455 100644 --- a/core/widgetdiv.ts +++ b/core/widgetdiv.ts @@ -8,6 +8,7 @@ import * as common from './common.js'; import {Field} from './field.js'; +import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js'; import * as dom from './utils/dom.js'; import type {Rect} from './utils/rect.js'; import type {Size} from './utils/size.js'; @@ -34,6 +35,9 @@ let themeClassName = ''; /** The HTML container for popup overlays (e.g. editor widgets). */ let containerDiv: HTMLDivElement | null; +/** Callback to FocusManager to return ephemeral focus when the div closes. */ +let returnEphemeralFocus: ReturnEphemeralFocus | null = null; + /** * Returns the HTML container for editor widgets. * @@ -110,6 +114,7 @@ export function show( if (themeClassName) { dom.addClass(div, themeClassName); } + returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div); } /** @@ -126,8 +131,14 @@ export function hide() { div.style.display = 'none'; div.style.left = ''; div.style.top = ''; - if (dispose) dispose(); - dispose = null; + if (returnEphemeralFocus) { + returnEphemeralFocus(); + returnEphemeralFocus = null; + } + if (dispose) { + dispose(); + dispose = null; + } div.textContent = ''; if (rendererClassName) { diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 921fb72ecc7..ee526bf8b6b 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2709,6 +2709,18 @@ export class WorkspaceSvg } } + const fieldIndicatorIndex = id.indexOf('_field_'); + if (fieldIndicatorIndex !== -1) { + const blockId = id.substring(0, fieldIndicatorIndex); + const block = this.getBlockById(blockId); + if (block) { + for (const field of block.getFields()) { + if (field.getFocusableElement().id === id) return field; + } + } + return null; + } + return this.getBlockById(id) as IFocusableNode; } From 0cbcc3144a33bf1f9be9fc00e1fa828292f4c436 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 30 Apr 2025 16:39:03 -0700 Subject: [PATCH 170/222] feat: Make connections focusable (#8928) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #8930 Fixes part of #8771 ### Proposed Changes This PR introduces support for connections to be focusable (and thus navigable with keyboard navigation when paired with downstream changes to `LineCursor` and the keyboard navigation plugin). This is a largely isolated change in how it fundamentally works: - `RenderedConnection` has been updated to be an `IFocusableNode` using a new unique ID maintained by `Connection` and automatically enabling/disabling the connection highlight based on whether it's focused (per keyboard navigation). - The way that rendering works here has changed: rather than recreating the connection's highlight SVG each time, it's only created once and updated thereafter to ensure that it correctly fits block resizes or movements. Visibility of the highlight is controlled entirely through display visibility and can now be done synchronously (which was a requirement for focusability as only displayed elements can be focused). - This employs the same type of ID schema strategy as fields in #8923. ### Reason for Changes This is part of an ongoing effort to ensure key components of Blockly are focusable so that they can be keyboard-navigable (with other needed changes yet both in Core Blockly and the keyboard navigation plugin). ### Test Coverage No new tests have been added. It's certainly possible to add unit tests for the focusable configurations being introduced in this PR, but it may not be highly beneficial. It's largely assumed that the individual implementations should work due to a highly tested FocusManager, and it may be the case that the interactions of the components working together is far more important to verify (that is, the end user flows). The latter is planned to be tackled as part of #8915. ### Documentation No documentation changes should be needed here. ### Additional Information This includes changes that have been pulled from #8875. --- core/connection.ts | 5 +++ core/rendered_connection.ts | 57 ++++++++++++++++++++++++-- core/renderers/common/drawer.ts | 13 +++--- core/renderers/common/i_path_object.ts | 2 +- core/renderers/common/path_object.ts | 36 ++++++++-------- core/renderers/zelos/drawer.ts | 9 ++-- core/workspace_svg.ts | 10 +++++ 7 files changed, 96 insertions(+), 36 deletions(-) diff --git a/core/connection.ts b/core/connection.ts index 039d8822c01..aed90e7c78c 100644 --- a/core/connection.ts +++ b/core/connection.ts @@ -20,6 +20,7 @@ import type {Input} from './inputs/input.js'; import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js'; import type {IConnectionChecker} from './interfaces/i_connection_checker.js'; import * as blocks from './serialization/blocks.js'; +import {idGenerator} from './utils.js'; import * as Xml from './xml.js'; /** @@ -55,6 +56,9 @@ export class Connection implements IASTNodeLocationWithBlock { /** DOM representation of a shadow block, or null if none. */ private shadowDom: Element | null = null; + /** The unique ID of this connection. */ + id: string; + /** * Horizontal location of this connection. * @@ -80,6 +84,7 @@ export class Connection implements IASTNodeLocationWithBlock { public type: number, ) { this.sourceBlock_ = source; + this.id = `${source.id}_connection_${idGenerator.getNextUniqueId()}`; } /** diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index 168e59744d2..7ada1b9f6b7 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -22,10 +22,13 @@ import * as ContextMenu from './contextmenu.js'; import {ContextMenuRegistry} from './contextmenu_registry.js'; import * as eventUtils from './events/utils.js'; import {IContextMenu} from './interfaces/i_contextmenu.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {hasBubble} from './interfaces/i_has_bubble.js'; import * as internalConstants from './internal_constants.js'; import {Coordinate} from './utils/coordinate.js'; import * as svgMath from './utils/svg_math.js'; +import {WorkspaceSvg} from './workspace_svg.js'; /** Maximum randomness in workspace units for bumping a block. */ const BUMP_RANDOMNESS = 10; @@ -33,7 +36,10 @@ const BUMP_RANDOMNESS = 10; /** * Class for a connection between blocks that may be rendered on screen. */ -export class RenderedConnection extends Connection implements IContextMenu { +export class RenderedConnection + extends Connection + implements IContextMenu, IFocusableNode +{ // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. sourceBlock_!: BlockSvg; private readonly db: ConnectionDB; @@ -320,13 +326,28 @@ export class RenderedConnection extends Connection implements IContextMenu { /** Add highlighting around this connection. */ highlight() { this.highlighted = true; - this.getSourceBlock().queueRender(); + + // Note that this needs to be done synchronously (vs. queuing a render pass) + // since only a displayed element can be focused, and this focusable node is + // implemented to make itself visible immediately prior to receiving DOM + // focus. It's expected that the connection's position should already be + // correct by this point (otherwise it will be corrected in a subsequent + // draw pass). + const highlightSvg = this.findHighlightSvg(); + if (highlightSvg) { + highlightSvg.style.display = ''; + } } /** Remove the highlighting around this connection. */ unhighlight() { this.highlighted = false; - this.getSourceBlock().queueRender(); + + // Note that this is done synchronously for parity with highlight(). + const highlightSvg = this.findHighlightSvg(); + if (highlightSvg) { + highlightSvg.style.display = 'none'; + } } /** Returns true if this connection is highlighted, false otherwise. */ @@ -626,6 +647,36 @@ export class RenderedConnection extends Connection implements IContextMenu { ContextMenu.show(e, menuOptions, block.RTL, workspace, location); } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + const highlightSvg = this.findHighlightSvg(); + if (highlightSvg) return highlightSvg; + throw new Error('No highlight SVG found corresponding to this connection.'); + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.getSourceBlock().workspace as WorkspaceSvg; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void { + this.highlight(); + } + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void { + this.unhighlight(); + } + + private findHighlightSvg(): SVGElement | null { + // This cast is valid as TypeScript's definition is wrong. See: + // https://github.com/microsoft/TypeScript/issues/60996. + return document.getElementById(this.id) as + | unknown + | null as SVGElement | null; + } } export namespace RenderedConnection { diff --git a/core/renderers/common/drawer.ts b/core/renderers/common/drawer.ts index 09320710c51..7046406adc7 100644 --- a/core/renderers/common/drawer.ts +++ b/core/renderers/common/drawer.ts @@ -435,19 +435,16 @@ export class Drawer { for (const elem of row.elements) { if (!(elem instanceof Connection)) continue; - if (elem.highlighted) { - this.drawConnectionHighlightPath(elem); - } else { - this.block_.pathObject.removeConnectionHighlight?.( - elem.connectionModel, - ); + const highlightSvg = this.drawConnectionHighlightPath(elem); + if (highlightSvg) { + highlightSvg.style.display = elem.highlighted ? '' : 'none'; } } } } /** Returns a path to highlight the given connection. */ - drawConnectionHighlightPath(measurable: Connection) { + drawConnectionHighlightPath(measurable: Connection): SVGElement | undefined { const conn = measurable.connectionModel; let path = ''; if ( @@ -459,7 +456,7 @@ export class Drawer { path = this.getStatementConnectionHighlightPath(measurable); } const block = conn.getSourceBlock(); - block.pathObject.addConnectionHighlight?.( + return block.pathObject.addConnectionHighlight?.( conn, path, conn.getOffsetInBlock(), diff --git a/core/renderers/common/i_path_object.ts b/core/renderers/common/i_path_object.ts index 699f1d92edb..776ba0067ea 100644 --- a/core/renderers/common/i_path_object.ts +++ b/core/renderers/common/i_path_object.ts @@ -113,7 +113,7 @@ export interface IPathObject { connectionPath: string, offset: Coordinate, rtl: boolean, - ): void; + ): SVGElement; /** * Apply the stored colours to the block's path, taking into account whether diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index 72cf2a594ce..ed2bb7dda75 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -268,37 +268,33 @@ export class PathObject implements IPathObject { connectionPath: string, offset: Coordinate, rtl: boolean, - ) { - if (this.connectionHighlights.has(connection)) { - if (this.currentHighlightMatchesNew(connection, connectionPath, offset)) { - return; - } - this.removeConnectionHighlight(connection); + ): SVGElement { + const transformation = + `translate(${offset.x}, ${offset.y})` + (rtl ? ' scale(-1 1)' : ''); + + const previousHighlight = this.connectionHighlights.get(connection); + if (previousHighlight) { + // Since a connection already exists, make sure that its path and + // transform are correct. + previousHighlight.setAttribute('d', connectionPath); + previousHighlight.setAttribute('transform', transformation); + return previousHighlight; } const highlight = dom.createSvgElement( Svg.PATH, { + 'id': connection.id, 'class': 'blocklyHighlightedConnectionPath', + 'style': 'display: none;', + 'tabindex': '-1', 'd': connectionPath, - 'transform': - `translate(${offset.x}, ${offset.y})` + (rtl ? ' scale(-1 1)' : ''), + 'transform': transformation, }, this.svgRoot, ); this.connectionHighlights.set(connection, highlight); - } - - private currentHighlightMatchesNew( - connection: RenderedConnection, - newPath: string, - newOffset: Coordinate, - ): boolean { - const currPath = this.connectionHighlights - .get(connection) - ?.getAttribute('d'); - const currOffset = this.highlightOffsets.get(connection); - return currPath === newPath && Coordinate.equals(currOffset, newOffset); + return highlight; } /** diff --git a/core/renderers/zelos/drawer.ts b/core/renderers/zelos/drawer.ts index 5cc52c0cbb2..b38711eb6c3 100644 --- a/core/renderers/zelos/drawer.ts +++ b/core/renderers/zelos/drawer.ts @@ -234,15 +234,16 @@ export class Drawer extends BaseDrawer { } /** Returns a path to highlight the given connection. */ - drawConnectionHighlightPath(measurable: Connection) { + override drawConnectionHighlightPath( + measurable: Connection, + ): SVGElement | undefined { const conn = measurable.connectionModel; if ( conn.type === ConnectionType.NEXT_STATEMENT || conn.type === ConnectionType.PREVIOUS_STATEMENT || (conn.type === ConnectionType.OUTPUT_VALUE && !measurable.isDynamicShape) ) { - super.drawConnectionHighlightPath(measurable); - return; + return super.drawConnectionHighlightPath(measurable); } let path = ''; @@ -261,7 +262,7 @@ export class Drawer extends BaseDrawer { (output.shape as DynamicShape).pathDown(output.height); } const block = conn.getSourceBlock(); - block.pathObject.addConnectionHighlight?.( + return block.pathObject.addConnectionHighlight?.( conn, path, conn.getOffsetInBlock(), diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index ee526bf8b6b..51992fcf0af 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2710,6 +2710,7 @@ export class WorkspaceSvg } const fieldIndicatorIndex = id.indexOf('_field_'); + const connectionIndicatorIndex = id.indexOf('_connection_'); if (fieldIndicatorIndex !== -1) { const blockId = id.substring(0, fieldIndicatorIndex); const block = this.getBlockById(blockId); @@ -2719,6 +2720,15 @@ export class WorkspaceSvg } } return null; + } else if (connectionIndicatorIndex !== -1) { + const blockId = id.substring(0, connectionIndicatorIndex); + const block = this.getBlockById(blockId); + if (block) { + for (const connection of block.getConnections_(true)) { + if (connection.id === id) return connection; + } + } + return null; } return this.getBlockById(id) as IFocusableNode; From 45c142636caf9cb8800284c771cf68ddc02ecc69 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 1 May 2025 11:18:27 -0700 Subject: [PATCH 171/222] fix: remove black outline on focused items (#8951) --- core/css.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/css.ts b/core/css.ts index ed47e8037ec..e1a2a10d307 100644 --- a/core/css.ts +++ b/core/css.ts @@ -494,4 +494,13 @@ input[type=number] { cursor: grabbing; } +.blocklyActiveFocus:is( + .blocklyFlyout, + .blocklyWorkspace, + .blocklyField, + .blocklyPath, + .blocklyHighlightedConnectionPath +) { + outline-width: 0px; +} `; From fdeaa7692b8f029fdca3155410b37bd2ed27ae82 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 1 May 2025 22:18:22 -0700 Subject: [PATCH 172/222] feat: Update line cursor to use focus manager (#8941) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #8940 Fixes #8954 Fixes #8955 ### Proposed Changes This updates `LineCursor` to use `FocusManager` rather than selection (principally) as the source of truth. ### Reason for Changes Ensuring that keyboard navigation works correctly with eventual screen reader support requires ensuring that ever navigated component is focused, and this is primarily what `FocusManager` has been designed to do. Since these nodes are already focused, `FocusManager` can be used as the primary source of truth for determining where the user currently has navigated, and where to go next. Previously, `LineCursor` relied on selection for this purpose, but selection is now automatically updated (for blocks) using focus-controlled `focus` and `blur` callbacks. Note that the cursor will still fall back to synchronizing with selection state, though this will be removed once the remaining work to eliminate `MarkerSvg` has concluded (which requires further consideration on the keyboard navigation side viz-a-viz styling and CSS decisions) and once mouse clicks are synchronized with focus management. Note that the changes in this PR are closely tied to https://github.com/google/blockly-keyboard-experimentation/pull/482 as both are necessary in order for the keyboard navigation plugin to correctly work with `FocusManager`. Some other noteworthy changes: - Some special handling exists for flyouts to handle navigating across stacks (per the current cursor design). - `FocusableTreeTraverser` is needed by the keyboard navigation plugin (in https://github.com/google/blockly-keyboard-experimentation/pull/482) so it's now being exported. - `FocusManager` had one bug that's now patched and tested in this PR: it didn't handle the case of the browser completely forcing focus loss. It would continue to maintain active focus even though no tracked elements now hold focus. One such case is the element being deleted, but there are other cases where this can happen (such as with dialog prompts). - `FocusManager` had some issues from #8909 wherein it would overeagerly call tree focus callbacks and slightly mismanage the passive node. Since tests haven't yet been added for these lifecycle callbacks, these cases weren't originally caught (per #8910). - `FocusManager` was updated to move the tracked manager into a static function so that it can be replaced in tests. This was done to facilitate changes to setup_teardown.js to ensure that a unique `FocusManager` exists _per-test_. It's possible for DOM focus state to still bleed across tests, but `FocusManager` largely guarantees eventual consistency. This change prevents a class of focus errors from being possible when running tests. - A number of cursor tests needed to be updated to ensure that a connections are properly rendered (as this is a requirement for focusable nodes, and cursor is now focusing nodes). One test for output connections was changed to use an input connection, instead, since output connections can no longer be navigated to (and aren't rendered, thus are not focusable). It's possible this will need to be changed in the future if we decide to reintroduce support for output connections in cursor, but it seems like a reasonable stopgap. Huge thanks to @rachel-fenichel for helping investigate and providing an alternative for the output connection test. **Current gaps** to be fixed after this PR is merged: - The flyout automatically closes when creating a variable with with keyboard or mouse (I think this is only for the keyboard navigation plugin). I believe this is a regression from previous behavior due to how the navigation plugin is managing state. It would know the flyout should be open and thus ensure it stays open even when things like dialog prompts try to close it with a blur event. However, the new implementation in https://github.com/google/blockly-keyboard-experimentation/pull/482 complicates this since state is now inferred from `FocusManager`, and the flyout _losing_ focus will force it closed. There was a fix introduced in this PR to fix it for keyboard navigation, but fails for clicks because the flyout never receives focus when the create variable button is clicked. It also caused the advanced compilation tests to fail due to a subtle circular dependency from importing `WorkspaceSvg` directly rather than its type. - The flyout, while it stays open, does not automatically update past the first variable being created without closing and reopening it. I'm actually not at all sure why this particular behavior has regressed. ### Test Coverage No new non-`FocusManager` tests have been added. It's certainly possible to add unit tests for the focusable configurations being introduced in this PR, but it may not be highly beneficial. It's largely assumed that the individual implementations should work due to a highly tested FocusManager, and it may be the case that the interactions of the components working together is far more important to verify (that is, the end user flows). The latter is planned to be tackled as part of #8915. Some new `FocusManager` tests were added, but more are still needed and this is tracked as part of #8910. ### Documentation No new documentation should be needed for these changes. ### Additional Information This includes changes that have been pulled from #8875. --- core/blockly.ts | 2 + core/focus_manager.ts | 88 ++++++++++------ core/keyboard_nav/line_cursor.ts | 114 +++++++-------------- tests/mocha/cursor_test.js | 19 ++-- tests/mocha/focus_manager_test.js | 76 +++++++++----- tests/mocha/test_helpers/setup_teardown.js | 23 +++++ 6 files changed, 181 insertions(+), 141 deletions(-) diff --git a/core/blockly.ts b/core/blockly.ts index c38a1d48e4b..069e21c81f6 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -120,6 +120,7 @@ import * as inputs from './inputs.js'; import {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import {LabelFlyoutInflater} from './label_flyout_inflater.js'; import {SeparatorFlyoutInflater} from './separator_flyout_inflater.js'; +import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.js'; import {Input} from './inputs/input.js'; import {InsertionMarkerPreviewer} from './insertion_marker_previewer.js'; @@ -527,6 +528,7 @@ export { FlyoutMetricsManager, FlyoutSeparator, FocusManager, + FocusableTreeTraverser, CodeGenerator as Generator, Gesture, Grid, diff --git a/core/focus_manager.ts b/core/focus_manager.ts index 88eef46b530..7091c4efb08 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -65,26 +65,15 @@ export class FocusManager { constructor( addGlobalEventListener: (type: string, listener: EventListener) => void, ) { - // Register root document focus listeners for tracking when focus leaves all - // tracked focusable trees. - addGlobalEventListener('focusin', (event) => { - if (!(event instanceof FocusEvent)) return; - - // The target that now has focus. - const activeElement = document.activeElement; + // Note that 'element' here is the element *gaining* focus. + const maybeFocus = (element: Element | EventTarget | null) => { let newNode: IFocusableNode | null | undefined = null; - if ( - activeElement instanceof HTMLElement || - activeElement instanceof SVGElement - ) { - // If the target losing focus maps to any tree, then it should be - // updated. Per the contract of findFocusableNodeFor only one tree - // should claim the element. + if (element instanceof HTMLElement || element instanceof SVGElement) { + // If the target losing or gaining focus maps to any tree, then it + // should be updated. Per the contract of findFocusableNodeFor only one + // tree should claim the element, so the search can be exited early. for (const tree of this.registeredTrees) { - newNode = FocusableTreeTraverser.findFocusableNodeFor( - activeElement, - tree, - ); + newNode = FocusableTreeTraverser.findFocusableNodeFor(element, tree); if (newNode) break; } } @@ -103,6 +92,26 @@ export class FocusManager { } else { this.defocusCurrentFocusedNode(); } + }; + + // Register root document focus listeners for tracking when focus leaves all + // tracked focusable trees. Note that focusin and focusout can be somewhat + // overlapping in the information that they provide. This is fine because + // they both aim to check for focus changes on the element gaining or having + // received focus, and maybeFocus should behave relatively deterministic. + addGlobalEventListener('focusin', (event) => { + if (!(event instanceof FocusEvent)) return; + + // When something receives focus, always use the current active element as + // the source of truth. + maybeFocus(document.activeElement); + }); + addGlobalEventListener('focusout', (event) => { + if (!(event instanceof FocusEvent)) return; + + // When something loses focus, it seems that document.activeElement may + // not necessarily be correct. Instead, use relatedTarget. + maybeFocus(event.relatedTarget); }); } @@ -247,7 +256,7 @@ export class FocusManager { const prevNode = this.focusedNode; const prevTree = prevNode?.getFocusableTree(); - if (prevNode && prevTree !== nextTree) { + if (prevNode) { this.passivelyFocusNode(prevNode, nextTree); } @@ -374,7 +383,9 @@ export class FocusManager { // node's focusable element (which *is* allowed to be invisible until the // node needs to be focused). this.lockFocusStateChanges = true; - node.getFocusableTree().onTreeFocus(node, prevTree); + if (node.getFocusableTree() !== prevTree) { + node.getFocusableTree().onTreeFocus(node, prevTree); + } node.onNodeFocus(); this.lockFocusStateChanges = false; @@ -399,11 +410,15 @@ export class FocusManager { nextTree: IFocusableTree | null, ): void { this.lockFocusStateChanges = true; - node.getFocusableTree().onTreeBlur(nextTree); + if (node.getFocusableTree() !== nextTree) { + node.getFocusableTree().onTreeBlur(nextTree); + } node.onNodeBlur(); this.lockFocusStateChanges = false; - this.setNodeToVisualPassiveFocus(node); + if (node.getFocusableTree() !== nextTree) { + this.setNodeToVisualPassiveFocus(node); + } } /** @@ -441,19 +456,24 @@ export class FocusManager { dom.removeClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); dom.removeClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); } -} -let focusManager: FocusManager | null = null; + private static focusManager: FocusManager | null = null; -/** - * Returns the page-global FocusManager. - * - * The returned instance is guaranteed to not change across function calls, but - * may change across page loads. - */ -export function getFocusManager(): FocusManager { - if (!focusManager) { - focusManager = new FocusManager(document.addEventListener); + /** + * Returns the page-global FocusManager. + * + * The returned instance is guaranteed to not change across function calls, but + * may change across page loads. + */ + static getFocusManager(): FocusManager { + if (!FocusManager.focusManager) { + FocusManager.focusManager = new FocusManager(document.addEventListener); + } + return FocusManager.focusManager; } - return focusManager; +} + +/** Convenience function for FocusManager.getFocusManager. */ +export function getFocusManager(): FocusManager { + return FocusManager.getFocusManager(); } diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 2025da7bf06..196a0854763 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -18,11 +18,9 @@ import {BlockSvg} from '../block_svg.js'; import * as common from '../common.js'; import type {Connection} from '../connection.js'; import {ConnectionType} from '../connection_type.js'; -import type {Abstract} from '../events/events_abstract.js'; -import {Click, ClickTarget} from '../events/events_click.js'; -import {EventType} from '../events/type.js'; -import * as eventUtils from '../events/utils.js'; import type {Field} from '../field.js'; +import {getFocusManager} from '../focus_manager.js'; +import {isFocusableNode} from '../interfaces/i_focusable_node.js'; import * as registry from '../registry.js'; import type {MarkerSvg} from '../renderers/common/marker_svg.js'; import type {PathObject} from '../renderers/zelos/path_object.js'; @@ -70,23 +68,12 @@ export class LineCursor extends Marker { options?: Partial, ) { super(); - // Bind changeListener to facilitate future disposal. - this.changeListener = this.changeListener.bind(this); - this.workspace.addChangeListener(this.changeListener); // Regularise options and apply defaults. this.options = {...defaultOptions, ...options}; this.isZelos = workspace.getRenderer() instanceof Renderer; } - /** - * Clean up this cursor. - */ - dispose() { - this.workspace.removeChangeListener(this.changeListener); - super.dispose(); - } - /** * Moves the cursor to the next previous connection, next connection or block * in the pre order traversal. Finds the next node in the pre order traversal. @@ -331,8 +318,8 @@ export class LineCursor extends Marker { * @param node The current position in the AST. * @param isValid A function true/false depending on whether the given node * should be traversed. - * @param loop Whether to loop around to the beginning of the workspace if - * novalid node was found. + * @param loop Whether to loop around to the beginning of the workspace if no + * valid node was found. * @returns The next node in the traversal. */ getNextNode( @@ -385,8 +372,8 @@ export class LineCursor extends Marker { * @param node The current position in the AST. * @param isValid A function true/false depending on whether the given node * should be traversed. - * @param loop Whether to loop around to the end of the workspace if no - * valid node was found. + * @param loop Whether to loop around to the end of the workspace if no valid + * node was found. * @returns The previous node in the traversal or null if no previous node * exists. */ @@ -527,7 +514,12 @@ export class LineCursor extends Marker { * @returns The current field, connection, or block the cursor is on. */ override getCurNode(): ASTNode | null { - this.updateCurNodeFromSelection(); + if (!this.updateCurNodeFromFocus()) { + // Fall back to selection if focus fails to sync. This can happen for + // non-focusable nodes or for cases when focus may not properly propagate + // (such as for mouse clicks). + this.updateCurNodeFromSelection(); + } return super.getCurNode(); } @@ -572,16 +564,15 @@ export class LineCursor extends Marker { * this.drawMarker() instead of this.drawer.draw() directly. * * @param newNode The new location of the cursor. - * @param updateSelection If true (the default) we'll update the selection - * too. */ - override setCurNode(newNode: ASTNode | null, updateSelection = true) { - if (updateSelection) { - this.updateSelectionFromNode(newNode); - } - + override setCurNode(newNode: ASTNode | null) { super.setCurNode(newNode); + const newNodeLocation = newNode?.getLocation(); + if (isFocusableNode(newNodeLocation)) { + getFocusManager().focusNode(newNodeLocation); + } + // Try to scroll cursor into view. if (newNode?.getType() === ASTNode.types.BLOCK) { const block = newNode.getLocation() as BlockSvg; @@ -709,32 +700,6 @@ export class LineCursor extends Marker { } } - /** - * Event listener that syncs the cursor location to the selected block on - * SELECTED events. - * - * This does not run early enough in all cases so `getCurNode()` also updates - * the node from the selection. - * - * @param event The `Selected` event. - */ - private changeListener(event: Abstract) { - switch (event.type) { - case EventType.SELECTED: - this.updateCurNodeFromSelection(); - break; - case EventType.CLICK: { - const click = event as Click; - if ( - click.workspaceId === this.workspace.id && - click.targetType === ClickTarget.WORKSPACE - ) { - this.setCurNode(null); - } - } - } - } - /** * Updates the current node to match the selection. * @@ -749,7 +714,7 @@ export class LineCursor extends Marker { const selected = common.getSelected(); if (selected === null && curNode?.getType() === ASTNode.types.BLOCK) { - this.setCurNode(null, false); + this.setCurNode(null); return; } if (selected?.workspace !== this.workspace) { @@ -770,36 +735,35 @@ export class LineCursor extends Marker { block = block.getParent(); } if (block) { - this.setCurNode(ASTNode.createBlockNode(block), false); + this.setCurNode(ASTNode.createBlockNode(block)); } } } /** - * Updates the selection from the node. + * Updates the current node to match what's currently focused. * - * Clears the selection for non-block nodes. - * Clears the selection for shadow blocks as the selection is drawn on - * the parent but the cursor will be drawn on the shadow block itself. - * We need to take care not to later clear the current node due to that null - * selection, so we track the latest selection we're in sync with. - * - * @param newNode The new node. + * @returns Whether the current node has been set successfully from the + * current focused node. */ - private updateSelectionFromNode(newNode: ASTNode | null) { - if (newNode?.getType() === ASTNode.types.BLOCK) { - if (common.getSelected() !== newNode.getLocation()) { - eventUtils.disable(); - common.setSelected(newNode.getLocation() as BlockSvg); - eventUtils.enable(); - } - } else { - if (common.getSelected()) { - eventUtils.disable(); - common.setSelected(null); - eventUtils.enable(); + private updateCurNodeFromFocus(): boolean { + const focused = getFocusManager().getFocusedNode(); + + if (focused instanceof BlockSvg) { + const block: BlockSvg | null = focused; + if (block && block.workspace === this.workspace) { + if (block.getRootBlock() === block && this.workspace.isFlyout) { + // This block actually represents a stack. Note that this is needed + // because ASTNode special cases stack for cross-block navigation. + this.setCurNode(ASTNode.createStackNode(block)); + } else { + this.setCurNode(ASTNode.createBlockNode(block)); + } + return true; } } + + return false; } /** diff --git a/tests/mocha/cursor_test.js b/tests/mocha/cursor_test.js index 53f0714da11..55595df7b05 100644 --- a/tests/mocha/cursor_test.js +++ b/tests/mocha/cursor_test.js @@ -6,6 +6,7 @@ import {ASTNode} from '../../build/src/core/keyboard_nav/ast_node.js'; import {assert} from '../../node_modules/chai/chai.js'; +import {createRenderedBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, sharedTestTeardown, @@ -63,11 +64,11 @@ suite('Cursor', function () { ]); this.workspace = Blockly.inject('blocklyDiv', {}); this.cursor = this.workspace.getCursor(); - const blockA = this.workspace.newBlock('input_statement'); - const blockB = this.workspace.newBlock('input_statement'); - const blockC = this.workspace.newBlock('input_statement'); - const blockD = this.workspace.newBlock('input_statement'); - const blockE = this.workspace.newBlock('field_input'); + const blockA = createRenderedBlock(this.workspace, 'input_statement'); + const blockB = createRenderedBlock(this.workspace, 'input_statement'); + const blockC = createRenderedBlock(this.workspace, 'input_statement'); + const blockD = createRenderedBlock(this.workspace, 'input_statement'); + const blockE = createRenderedBlock(this.workspace, 'field_input'); blockA.nextConnection.connect(blockB.previousConnection); blockA.inputList[0].connection.connect(blockE.outputConnection); @@ -105,12 +106,12 @@ suite('Cursor', function () { ); }); - test('In - From output connection', function () { + test('In - From attached input connection', function () { const fieldBlock = this.blocks.E; - const outputNode = ASTNode.createConnectionNode( - fieldBlock.outputConnection, + const inputConnectionNode = ASTNode.createConnectionNode( + this.blocks.A.inputList[0].connection, ); - this.cursor.setCurNode(outputNode); + this.cursor.setCurNode(inputConnectionNode); this.cursor.in(); const curNode = this.cursor.getCurNode(); assert.equal(curNode.getLocation(), fieldBlock); diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js index 69ecfe722a5..bcd74c1a03c 100644 --- a/tests/mocha/focus_manager_test.js +++ b/tests/mocha/focus_manager_test.js @@ -71,26 +71,20 @@ suite('FocusManager', function () { const ACTIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME}`; const PASSIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME}`; + const createFocusableTree = function (rootElementId, nestedTrees) { + return new FocusableTreeImpl( + document.getElementById(rootElementId), + nestedTrees || [], + ); + }; + const createFocusableNode = function (tree, elementId) { + return tree.addNode(document.getElementById(elementId)); + }; + setup(function () { sharedTestSetup.call(this); - const testState = this; - const addDocumentEventListener = function (type, listener) { - testState.globalDocumentEventListenerType = type; - testState.globalDocumentEventListener = listener; - document.addEventListener(type, listener); - }; - this.focusManager = new FocusManager(addDocumentEventListener); - - const createFocusableTree = function (rootElementId, nestedTrees) { - return new FocusableTreeImpl( - document.getElementById(rootElementId), - nestedTrees || [], - ); - }; - const createFocusableNode = function (tree, elementId) { - return tree.addNode(document.getElementById(elementId)); - }; + this.focusManager = getFocusManager(); this.testFocusableTree1 = createFocusableTree('testFocusableTree1'); this.testFocusableTree1Node1 = createFocusableNode( @@ -160,12 +154,6 @@ suite('FocusManager', function () { teardown(function () { sharedTestTeardown.call(this); - // Remove the globally registered listener from FocusManager to avoid state being shared across - // test boundaries. - const eventType = this.globalDocumentEventListenerType; - const eventListener = this.globalDocumentEventListener; - document.removeEventListener(eventType, eventListener); - // Ensure all node CSS styles are reset so that state isn't leaked between tests. const activeElems = document.querySelectorAll( ACTIVE_FOCUS_NODE_CSS_SELECTOR, @@ -832,6 +820,27 @@ suite('FocusManager', function () { this.testFocusableNestedTree4Node1, ); }); + + test('deletion after focusNode() returns null', function () { + const rootElem = document.createElement('div'); + const nodeElem = document.createElement('div'); + rootElem.setAttribute('id', 'focusRoot'); + rootElem.setAttribute('tabindex', '-1'); + nodeElem.setAttribute('id', 'focusNode'); + nodeElem.setAttribute('tabindex', '-1'); + nodeElem.textContent = 'Focusable node'; + rootElem.appendChild(nodeElem); + document.body.appendChild(rootElem); + const root = createFocusableTree('focusRoot'); + const node = createFocusableNode(root, 'focusNode'); + this.focusManager.registerTree(root); + this.focusManager.focusNode(node); + + node.getFocusableElement().remove(); + + assert.notStrictEqual(this.focusManager.getFocusedNode(), node); + rootElem.remove(); // Cleanup. + }); }); suite('CSS classes', function () { test('registered tree focusTree()ed no prev focus root elem has active property', function () { @@ -1724,6 +1733,27 @@ suite('FocusManager', function () { this.testFocusableNestedTree4Node1, ); }); + + test('deletion after focus() returns null', function () { + const rootElem = document.createElement('div'); + const nodeElem = document.createElement('div'); + rootElem.setAttribute('id', 'focusRoot'); + rootElem.setAttribute('tabindex', '-1'); + nodeElem.setAttribute('id', 'focusNode'); + nodeElem.setAttribute('tabindex', '-1'); + nodeElem.textContent = 'Focusable node'; + rootElem.appendChild(nodeElem); + document.body.appendChild(rootElem); + const root = createFocusableTree('focusRoot'); + const node = createFocusableNode(root, 'focusNode'); + this.focusManager.registerTree(root); + document.getElementById('focusNode').focus(); + + node.getFocusableElement().remove(); + + assert.notStrictEqual(this.focusManager.getFocusedNode(), node); + rootElem.remove(); // Cleanup. + }); }); suite('CSS classes', function () { test('registered root focus()ed no prev focus returns root elem has active property', function () { diff --git a/tests/mocha/test_helpers/setup_teardown.js b/tests/mocha/test_helpers/setup_teardown.js index 2fc08cb6943..b0d7c83c697 100644 --- a/tests/mocha/test_helpers/setup_teardown.js +++ b/tests/mocha/test_helpers/setup_teardown.js @@ -5,6 +5,7 @@ */ import * as eventUtils from '../../../build/src/core/events/utils.js'; +import {FocusManager} from '../../../build/src/core/focus_manager.js'; /** * Safely disposes of Blockly workspace, logging any errors. @@ -124,6 +125,18 @@ export function sharedTestSetup(options = {}) { }; this.blockTypesCleanup_ = this.sharedCleanup.blockTypesCleanup_; this.messagesCleanup_ = this.sharedCleanup.messagesCleanup_; + + // Set up FocusManager to run in isolation for this test. + this.globalDocumentEventListeners = []; + const testState = this; + const addDocumentEventListener = function (type, listener) { + testState.globalDocumentEventListeners.push({type, listener}); + document.addEventListener(type, listener); + }; + const specificFocusManager = new FocusManager(addDocumentEventListener); + this.oldGetFocusManager = FocusManager.getFocusManager; + FocusManager.getFocusManager = () => specificFocusManager; + wrapDefineBlocksWithJsonArrayWithCleanup_(this.sharedCleanup); return { clock: this.clock, @@ -184,6 +197,16 @@ export function sharedTestTeardown() { } Blockly.WidgetDiv.testOnly_setDiv(null); + + // Remove the globally registered listener from FocusManager to avoid state + // being shared across test boundaries. + for (const registeredListener of this.globalDocumentEventListeners) { + const eventType = registeredListener.type; + const eventListener = registeredListener.listener; + document.removeEventListener(eventType, eventListener); + } + this.globalDocumentEventListeners = []; + FocusManager.getFocusManager = this.oldGetFocusManager; } } From 778b7d50e1975a23ffa8251b4c90a3fe9fa7660b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 2 May 2025 08:05:34 -0700 Subject: [PATCH 173/222] fix: Fix conventional auto labeling (#8956) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves This fixes the ongoing CI failure for the conventional auto-labeling. ### Proposed Changes This fixes the permissions in a way that should work. It may be the case that 'issues' only needs to be 'read' but I'm basically just copying what's done in https://github.com/GoogleCloudPlatform/developer-journey-app/blob/main/.github/workflows/auto-label.yml since it's working for them. ### Reason for Changes We want this workflow working--it's preferable to avoid getting used to a failing CI workflow (ideally every PR has zero CI failures). As for the specific changes, note that the check will still fail in this PR. Similar to #8811, it's not expected that the CI workflow will pass in this PR until the change is checked in since the workflow uses 'pull_request_target'. While I haven't verified this change directly, I'm fairly confident it will work given the project linked above and our successes with fixing the auto assigner workflow. Finally, the 'contents: read' bit is unnecessary since that's the default permission for `GITHUB_TOKEN` per https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token. Edit: It seems that the check actually is passing with these changes--that's a bit surprising to me. ### Test Coverage N/A ### Documentation N/A ### Additional Information None. --- .github/workflows/conventional-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/conventional-label.yml b/.github/workflows/conventional-label.yml index 57eaaa6980c..42dcf26645b 100644 --- a/.github/workflows/conventional-label.yml +++ b/.github/workflows/conventional-label.yml @@ -8,7 +8,7 @@ jobs: label: runs-on: ubuntu-latest permissions: - contents: read + issues: write pull-requests: write steps: - uses: bcoe/conventional-release-labels@v1 From 7b4f2239d79a95061827ca3d4347599713a6dd10 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Fri, 2 May 2025 17:40:45 +0100 Subject: [PATCH 174/222] feat(WorkspaceSvg): Add support for tracking keyboard moves (#8959) --- core/workspace_svg.ts | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 51992fcf0af..39187e66d54 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -317,6 +317,9 @@ export class WorkspaceSvg /** True if keyboard accessibility mode is on, false otherwise. */ keyboardAccessibilityMode = false; + /** True iff a keyboard-initiated move ("drag") is in progress. */ + keyboardMoveInProgress = false; + /** The list of top-level bounded elements on the workspace. */ private topBoundedElements: IBoundedElement[] = []; @@ -1505,12 +1508,43 @@ export class WorkspaceSvg } /** - * Is the user currently dragging a block or scrolling the flyout/workspace? + * Indicate whether a keyboard move is in progress or not. + * + * Should be called with true when a keyboard move of an IDraggable + * is starts, and false when it finishes or is aborted. * - * @returns True if currently dragging or scrolling. + * N.B.: This method is experimental and internal-only. It is + * intended only to called only from the keyboard navigation plugin. + * Its signature and behaviour may be modified, or the method + * removed, at an time without notice and without being treated + * as a breaking change. + * + * @internal + * @param inProgress Is a keyboard-initated move in progress? + */ + setKeyboardMoveInProgress(inProgress: boolean) { + this.keyboardMoveInProgress = inProgress; + } + + /** + * Returns true iff the user is currently engaged in a drag gesture, + * or if a keyboard-initated move is in progress. + * + * Dragging gestures normally entail moving a block or other item on + * the workspace, or scrolling the flyout/workspace. + * + * Keyboard-initated movements are implemnted using the dragging + * infrastructure and are intended to emulate (a subset of) drag + * gestures and so should typically be treated as if they were a + * gesture-based drag. + * + * @returns True iff a drag gesture or keyboard move is in porgress. */ isDragging(): boolean { - return this.currentGesture_ !== null && this.currentGesture_.isDragging(); + return ( + this.keyboardMoveInProgress || + (this.currentGesture_ !== null && this.currentGesture_.isDragging()) + ); } /** From 3d1d80d6616882339adaa594c217be1febade433 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Fri, 2 May 2025 17:47:11 +0100 Subject: [PATCH 175/222] refactor!: Finish refactor of `WorkspaceSvg` `VariableMap` methods (#8946) * docs: Make JSDoc @deprecated usage more consistent * refactor(VariableMap)!: Refresh toolbox when map modified Delete the following methods from WorkspaceSvg: - renameVariableById - deleteVariableById - createVariable Modify the following methods on VariableMap to call this.workspace.refreshToolboxSelection() if this.workspace is a WorkspaceSvg, replicating the behaviour of the aforementioned deleted methods and additionally ensuring that that method is called following any change to the variable map: - renameVariable - changeVariableType - renameVariableAndUses - createVariable - addVariable - deleteVariable BREAKING CHANGE: This change ensures that the toolbox will be refreshed regardless of what route the VaribleMap was updated, rather than only being refreshed when it is updated via calls to methods on WorkspaceSvg. Overall this is much more likely to fix a bug (where the toolbox wasn't being refreshed when it should have been) than cause one (by refreshing the toolbox when it shouldn't be), but this is still a behaviour change which could _conceivably_ result an unexpected regression. * refactor(VariableMap): Remove calls to deprecated getVariableUsesById Also refactor to use named imports core/variables.ts methods. * refactor(Workspace): Use named imports for core/variables.ts methods * refactor(FieldVariable): Remove call to deprecated getVariablesOfType * refactor(variables): Remove calls to deprecated methods * refactor(variables_dynamic): Remove call to deprecated getAllVariables * refactor(xml): Remove calls to deprecated createVariable * refactor(Events.VarCreate): Remove calls to deprecated methods * refactor(Events.VarDelete): Remove calls to deprecated methods * refactor(Events.VarRename): Remove calls to deprecated methods --- core/block_svg.ts | 4 ++- core/events/events_var_create.ts | 6 ++-- core/events/events_var_delete.ts | 6 ++-- core/events/events_var_rename.ts | 6 ++-- core/field_variable.ts | 4 ++- core/variable_map.ts | 36 ++++++++++++++++++------ core/variables.ts | 20 ++++++++----- core/variables_dynamic.ts | 4 +-- core/workspace.ts | 25 +++++++++-------- core/workspace_svg.ts | 48 -------------------------------- core/xml.ts | 2 +- 11 files changed, 74 insertions(+), 87 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 04b7d88d1f2..151ee868fea 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -1083,13 +1083,15 @@ export class BlockSvg } /** - * @deprecated v11 - Set whether the block is manually enabled or disabled. + * Set whether the block is manually enabled or disabled. + * * The user can toggle whether a block is disabled from a context menu * option. A block may still be disabled for other reasons even if the user * attempts to manually enable it, such as when the block is in an invalid * location. This method is deprecated and setDisabledReason should be used * instead. * + * @deprecated v11: use setDisabledReason. * @param enabled True if enabled. */ override setEnabled(enabled: boolean) { diff --git a/core/events/events_var_create.ts b/core/events/events_var_create.ts index 3140f141004..c34c7ff57ae 100644 --- a/core/events/events_var_create.ts +++ b/core/events/events_var_create.ts @@ -113,10 +113,12 @@ export class VarCreate extends VarBase { 'the constructor, or call fromJson', ); } + const variableMap = workspace.getVariableMap(); if (forward) { - workspace.createVariable(this.varName, this.varType, this.varId); + variableMap.createVariable(this.varName, this.varType, this.varId); } else { - workspace.deleteVariableById(this.varId); + const variable = variableMap.getVariableById(this.varId); + if (variable) variableMap.deleteVariable(variable); } } } diff --git a/core/events/events_var_delete.ts b/core/events/events_var_delete.ts index 225459c44c7..62317e36c50 100644 --- a/core/events/events_var_delete.ts +++ b/core/events/events_var_delete.ts @@ -106,10 +106,12 @@ export class VarDelete extends VarBase { 'the constructor, or call fromJson', ); } + const variableMap = workspace.getVariableMap(); if (forward) { - workspace.deleteVariableById(this.varId); + const variable = variableMap.getVariableById(this.varId); + if (variable) variableMap.deleteVariable(variable); } else { - workspace.createVariable(this.varName, this.varType, this.varId); + variableMap.createVariable(this.varName, this.varType, this.varId); } } } diff --git a/core/events/events_var_rename.ts b/core/events/events_var_rename.ts index 23a0a17cdc2..a1758738c22 100644 --- a/core/events/events_var_rename.ts +++ b/core/events/events_var_rename.ts @@ -115,10 +115,12 @@ export class VarRename extends VarBase { 'the constructor, or call fromJson', ); } + const variableMap = workspace.getVariableMap(); + const variable = variableMap.getVariableById(this.varId); if (forward) { - workspace.renameVariableById(this.varId, this.newName); + if (variable) variableMap.renameVariable(variable, this.newName); } else { - workspace.renameVariableById(this.varId, this.oldName); + if (variable) variableMap.renameVariable(variable, this.oldName); } } } diff --git a/core/field_variable.ts b/core/field_variable.ts index 2af3c4d057a..83a235597a5 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -586,7 +586,9 @@ export class FieldVariable extends FieldDropdown { // doesn't modify the workspace's list. for (let i = 0; i < variableTypes.length; i++) { const variableType = variableTypes[i]; - const variables = workspace.getVariablesOfType(variableType); + const variables = workspace + .getVariableMap() + .getVariablesOfType(variableType); variableModelList = variableModelList.concat(variables); if (workspace.isFlyout) { variableModelList = variableModelList.concat( diff --git a/core/variable_map.ts b/core/variable_map.ts index 40efc3ea6e5..6195034775e 100644 --- a/core/variable_map.ts +++ b/core/variable_map.ts @@ -25,8 +25,9 @@ import {Names} from './names.js'; import * as registry from './registry.js'; import * as deprecation from './utils/deprecation.js'; import * as idGenerator from './utils/idgenerator.js'; -import * as Variables from './variables.js'; +import {deleteVariable, getVariableUsesById} from './variables.js'; import type {Workspace} from './workspace.js'; +import {WorkspaceSvg} from './workspace_svg.js'; /** * Class for a variable map. This contains a dictionary data structure with @@ -92,6 +93,9 @@ export class VariableMap } finally { eventUtils.setGroup(existingGroup); } + if (this.workspace instanceof WorkspaceSvg) { + this.workspace.refreshToolboxSelection(); + } return variable; } @@ -108,7 +112,9 @@ export class VariableMap if (!this.variableMap.has(newType)) { this.variableMap.set(newType, newTypeVariables); } - + if (this.workspace instanceof WorkspaceSvg) { + this.workspace.refreshToolboxSelection(); + } return variable; } @@ -116,7 +122,7 @@ export class VariableMap * Rename a variable by updating its name in the variable map. Identify the * variable to rename with the given ID. * - * @deprecated v12, use VariableMap.renameVariable. + * @deprecated v12: use VariableMap.renameVariable. * @param id ID of the variable to rename. * @param newName New variable name. */ @@ -155,6 +161,9 @@ export class VariableMap for (let i = 0; i < blocks.length; i++) { blocks[i].updateVarName(variable); } + if (this.workspace instanceof WorkspaceSvg) { + this.workspace.refreshToolboxSelection(); + } } /** @@ -250,6 +259,9 @@ export class VariableMap this.variableMap.set(type, variables); } eventUtils.fire(new (eventUtils.get(EventType.VAR_CREATE))(variable)); + if (this.workspace instanceof WorkspaceSvg) { + this.workspace.refreshToolboxSelection(); + } return variable; } @@ -267,6 +279,9 @@ export class VariableMap ); } this.variableMap.get(type)?.set(variable.getId(), variable); + if (this.workspace instanceof WorkspaceSvg) { + this.workspace.refreshToolboxSelection(); + } } /* Begin functions for variable deletion. */ @@ -276,7 +291,7 @@ export class VariableMap * @param variable Variable to delete. */ deleteVariable(variable: IVariableModel) { - const uses = this.getVariableUsesById(variable.getId()); + const uses = getVariableUsesById(this.workspace, variable.getId()); const existingGroup = eventUtils.getGroup(); if (!existingGroup) { eventUtils.setGroup(true); @@ -295,13 +310,16 @@ export class VariableMap } finally { eventUtils.setGroup(existingGroup); } + if (this.workspace instanceof WorkspaceSvg) { + this.workspace.refreshToolboxSelection(); + } } /** * Delete a variables by the passed in ID and all of its uses from this * workspace. May prompt the user for confirmation. * - * @deprecated v12, use Blockly.Variables.deleteVariable. + * @deprecated v12: use Blockly.Variables.deleteVariable. * @param id ID of variable to delete. */ deleteVariableById(id: string) { @@ -313,7 +331,7 @@ export class VariableMap ); const variable = this.getVariableById(id); if (variable) { - Variables.deleteVariable(this.workspace, variable); + deleteVariable(this.workspace, variable); } } @@ -398,7 +416,7 @@ export class VariableMap /** * Returns all of the variable names of all types. * - * @deprecated v12, use Blockly.Variables.getAllVariables. + * @deprecated v12: use Blockly.Variables.getAllVariables. * @returns All of the variable names of all types. */ getAllVariableNames(): string[] { @@ -420,7 +438,7 @@ export class VariableMap /** * Find all the uses of a named variable. * - * @deprecated v12, use Blockly.Variables.getVariableUsesById. + * @deprecated v12: use Blockly.Variables.getVariableUsesById. * @param id ID of the variable to find. * @returns Array of block usages. */ @@ -431,7 +449,7 @@ export class VariableMap 'v13', 'Blockly.Variables.getVariableUsesById', ); - return Variables.getVariableUsesById(this.workspace, id); + return getVariableUsesById(this.workspace, id); } } diff --git a/core/variables.ts b/core/variables.ts index c896efd0f1a..f75d673903b 100644 --- a/core/variables.ts +++ b/core/variables.ts @@ -31,8 +31,9 @@ export const CATEGORY_NAME = 'VARIABLE'; /** * Find all user-created variables that are in use in the workspace. * For use by generators. + * * To get a list of all variables on a workspace, including unused variables, - * call Workspace.getAllVariables. + * call getAllVariables. * * @param ws The workspace to search for variables. * @returns Array of variable models. @@ -61,6 +62,7 @@ export function allUsedVarModels( /** * Find all developer variables used by blocks in the workspace. + * * Developer variables are never shown to the user, but are declared as global * variables in the generated code. * To declare developer variables, define the getDeveloperVariables function on @@ -146,7 +148,7 @@ export function flyoutCategory( }, ...jsonFlyoutCategoryBlocks( workspace, - workspace.getVariablesOfType(''), + workspace.getVariableMap().getVariablesOfType(''), true, ), ]; @@ -268,7 +270,7 @@ function xmlFlyoutCategory(workspace: WorkspaceSvg): Element[] { * @returns Array of XML block elements. */ export function flyoutCategoryBlocks(workspace: Workspace): Element[] { - const variableModelList = workspace.getVariablesOfType(''); + const variableModelList = workspace.getVariableMap().getVariablesOfType(''); const xmlList = []; if (variableModelList.length > 0) { @@ -332,7 +334,10 @@ export function generateUniqueName(workspace: Workspace): string { function generateUniqueNameInternal(workspace: Workspace): string { return generateUniqueNameFromOptions( VAR_LETTER_OPTIONS.charAt(0), - workspace.getAllVariableNames(), + workspace + .getVariableMap() + .getAllVariables() + .map((v) => v.getName()), ); } @@ -415,7 +420,7 @@ export function createVariableButtonHandler( const existing = nameUsedWithAnyType(text, workspace); if (!existing) { // No conflict - workspace.createVariable(text, type); + workspace.getVariableMap().createVariable(text, type); if (opt_callback) opt_callback(text); return; } @@ -478,7 +483,7 @@ export function renameVariable( ); if (!existing && !procedure) { // No conflict. - workspace.renameVariableById(variable.getId(), newName); + workspace.getVariableMap().renameVariable(variable, newName); if (opt_callback) opt_callback(newName); return; } @@ -762,6 +767,7 @@ function createVariable( opt_name?: string, opt_type?: string, ): IVariableModel { + const variableMap = workspace.getVariableMap(); const potentialVariableMap = workspace.getPotentialVariableMap(); // Variables without names get uniquely named for this workspace. if (!opt_name) { @@ -781,7 +787,7 @@ function createVariable( ); } else { // In the main workspace, create a real variable. - variable = workspace.createVariable(opt_name, opt_type, id); + variable = variableMap.createVariable(opt_name, opt_type, id); } return variable; } diff --git a/core/variables_dynamic.ts b/core/variables_dynamic.ts index 4e2682ce8e9..f8169d28123 100644 --- a/core/variables_dynamic.ts +++ b/core/variables_dynamic.ts @@ -148,7 +148,7 @@ export function flyoutCategory( }, ...Variables.jsonFlyoutCategoryBlocks( workspace, - workspace.getAllVariables(), + workspace.getVariableMap().getAllVariables(), false, 'variables_get_dynamic', 'variables_set_dynamic', @@ -203,7 +203,7 @@ function xmlFlyoutCategory(workspace: WorkspaceSvg): Element[] { * @returns Array of XML block elements. */ export function flyoutCategoryBlocks(workspace: Workspace): Element[] { - const variableModelList = workspace.getAllVariables(); + const variableModelList = workspace.getVariableMap().getAllVariables(); const xmlList = []; if (variableModelList.length > 0) { diff --git a/core/workspace.ts b/core/workspace.ts index 261da0f2475..5ae4f8e2b14 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -37,7 +37,7 @@ import * as deprecation from './utils/deprecation.js'; import * as idGenerator from './utils/idgenerator.js'; import * as math from './utils/math.js'; import type * as toolbox from './utils/toolbox.js'; -import * as Variables from './variables.js'; +import {deleteVariable, getVariableUsesById} from './variables.js'; /** * Class for a workspace. This is a data structure that contains blocks. @@ -386,9 +386,10 @@ export class Workspace implements IASTNodeLocation { /* Begin functions that are just pass-throughs to the variable map. */ /** - * @deprecated v12 - Rename a variable by updating its name in the variable + * Rename a variable by updating its name in the variable * map. Identify the variable to rename with the given ID. * + * @deprecated v12: use Blockly.Workspace.getVariableMap().renameVariable * @param id ID of the variable to rename. * @param newName New variable name. */ @@ -407,7 +408,7 @@ export class Workspace implements IASTNodeLocation { /** * Create a variable with a given name, optional type, and optional ID. * - * @deprecated v12, use Blockly.Workspace.getVariableMap().createVariable. + * @deprecated v12: use Blockly.Workspace.getVariableMap().createVariable. * @param name The name of the variable. This must be unique across variables * and procedures. * @param opt_type The type of the variable like 'int' or 'string'. @@ -437,7 +438,7 @@ export class Workspace implements IASTNodeLocation { /** * Find all the uses of the given variable, which is identified by ID. * - * @deprecated v12, use Blockly.Workspace.getVariableMap().getVariableUsesById + * @deprecated v12: use Blockly.Workspace.getVariableMap().getVariableUsesById * @param id ID of the variable to find. * @returns Array of block usages. */ @@ -448,14 +449,14 @@ export class Workspace implements IASTNodeLocation { 'v13', 'Blockly.Workspace.getVariableMap().getVariableUsesById', ); - return Variables.getVariableUsesById(this, id); + return getVariableUsesById(this, id); } /** * Delete a variables by the passed in ID and all of its uses from this * workspace. May prompt the user for confirmation. * - * @deprecated v12, use Blockly.Workspace.getVariableMap().deleteVariable. + * @deprecated v12: use Blockly.Workspace.getVariableMap().deleteVariable. * @param id ID of variable to delete. */ deleteVariableById(id: string) { @@ -470,14 +471,14 @@ export class Workspace implements IASTNodeLocation { console.warn(`Can't delete non-existent variable: ${id}`); return; } - Variables.deleteVariable(this, variable); + deleteVariable(this, variable); } /** * Find the variable by the given name and return it. Return null if not * found. * - * @deprecated v12, use Blockly.Workspace.getVariableMap().getVariable. + * @deprecated v12: use Blockly.Workspace.getVariableMap().getVariable. * @param name The name to check for. * @param opt_type The type of the variable. If not provided it defaults to * the empty string, which is a specific type. @@ -500,7 +501,7 @@ export class Workspace implements IASTNodeLocation { /** * Find the variable by the given ID and return it. Return null if not found. * - * @deprecated v12, use Blockly.Workspace.getVariableMap().getVariableById. + * @deprecated v12: use Blockly.Workspace.getVariableMap().getVariableById. * @param id The ID to check for. * @returns The variable with the given ID. */ @@ -518,7 +519,7 @@ export class Workspace implements IASTNodeLocation { * Find the variable with the specified type. If type is null, return list of * variables with empty string type. * - * @deprecated v12, use Blockly.Workspace.getVariableMap().getVariablesOfType. + * @deprecated v12: use Blockly.Workspace.getVariableMap().getVariablesOfType. * @param type Type of the variables to find. * @returns The sought after variables of the passed in type. An empty array * if none are found. @@ -536,7 +537,7 @@ export class Workspace implements IASTNodeLocation { /** * Return all variables of all types. * - * @deprecated v12, use Blockly.Workspace.getVariableMap().getAllVariables. + * @deprecated v12: use Blockly.Workspace.getVariableMap().getAllVariables. * @returns List of variable models. */ getAllVariables(): IVariableModel[] { @@ -552,7 +553,7 @@ export class Workspace implements IASTNodeLocation { /** * Returns all variable names of all types. * - * @deprecated v12, use Blockly.Workspace.getVariableMap().getAllVariables. + * @deprecated v12: use Blockly.Workspace.getVariableMap().getAllVariables. * @returns List of all variable names of all types. */ getAllVariableNames(): string[] { diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 39187e66d54..0ae0b5b6ef3 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -52,10 +52,6 @@ import { import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; import type {IToolbox} from './interfaces/i_toolbox.js'; -import type { - IVariableModel, - IVariableState, -} from './interfaces/i_variable_model.js'; import type {LineCursor} from './keyboard_nav/line_cursor.js'; import type {Marker} from './keyboard_nav/marker.js'; import {LayerManager} from './layer_manager.js'; @@ -1364,50 +1360,6 @@ export class WorkspaceSvg } } - /** - * Rename a variable by updating its name in the variable map. Update the - * flyout to show the renamed variable immediately. - * - * @param id ID of the variable to rename. - * @param newName New variable name. - */ - override renameVariableById(id: string, newName: string) { - super.renameVariableById(id, newName); - this.refreshToolboxSelection(); - } - - /** - * Delete a variable by the passed in ID. Update the flyout to show - * immediately that the variable is deleted. - * - * @param id ID of variable to delete. - */ - override deleteVariableById(id: string) { - super.deleteVariableById(id); - this.refreshToolboxSelection(); - } - - /** - * Create a new variable with the given name. Update the flyout to show the - * new variable immediately. - * - * @param name The new variable's name. - * @param opt_type The type of the variable like 'int' or 'string'. - * Does not need to be unique. Field_variable can filter variables based - * on their type. This will default to '' which is a specific type. - * @param opt_id The unique ID of the variable. This will default to a UUID. - * @returns The newly created variable. - */ - override createVariable( - name: string, - opt_type?: string | null, - opt_id?: string | null, - ): IVariableModel { - const newVar = super.createVariable(name, opt_type, opt_id); - this.refreshToolboxSelection(); - return newVar; - } - /** Make a list of all the delete areas for this workspace. */ recordDragTargets() { const dragTargets = this.componentManager.getComponents( diff --git a/core/xml.ts b/core/xml.ts index f4b5f66ddd2..cc26d8c8a2c 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -703,7 +703,7 @@ export function domToVariables(xmlVariables: Element, workspace: Workspace) { const name = xmlChild.textContent; if (!name) return; - workspace.createVariable(name, type, id); + workspace.getVariableMap().createVariable(name, type ?? undefined, id); } } From 1c79e1ed77e5a341abbb01a5807abe71079a2297 Mon Sep 17 00:00:00 2001 From: Robert Knight <95928279+microbit-robert@users.noreply.github.com> Date: Fri, 2 May 2025 18:17:11 +0100 Subject: [PATCH 176/222] fix: Address remaining invisible input positions (#8948) --- core/keyboard_nav/ast_node.ts | 37 +++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/core/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts index 009e8f5b1e4..6844b87f954 100644 --- a/core/keyboard_nav/ast_node.ts +++ b/core/keyboard_nav/ast_node.ts @@ -131,6 +131,10 @@ export class ASTNode { return this.isConnectionLocation; } + private getVisibleInputs(block: Block): Input[] { + return block.inputList.filter((input) => input.isVisible()); + } + /** * Given an input find the next editable field or an input with a non null * connection in the same block. The current location must be an input @@ -145,9 +149,10 @@ export class ASTNode { const block = parentInput?.getSourceBlock(); if (!block || !parentInput) return null; - const curIdx = block.inputList.indexOf(parentInput); - for (let i = curIdx + 1; i < block.inputList.length; i++) { - const input = block.inputList[i]; + const visibleInputs = this.getVisibleInputs(block); + const curIdx = visibleInputs.indexOf(parentInput); + for (let i = curIdx + 1; i < visibleInputs.length; i++) { + const input = visibleInputs[i]; const fieldRow = input.fieldRow; for (let j = 0; j < fieldRow.length; j++) { const field = fieldRow[j]; @@ -178,10 +183,11 @@ export class ASTNode { 'The current AST location is not associated with a block', ); } - const curIdx = block.inputList.indexOf(input); + const visibleInputs = this.getVisibleInputs(block); + const curIdx = visibleInputs.indexOf(input); let fieldIdx = input.fieldRow.indexOf(location) + 1; - for (let i = curIdx; i < block.inputList.length; i++) { - const newInput = block.inputList[i]; + for (let i = curIdx; i < visibleInputs.length; i++) { + const newInput = visibleInputs[i]; const fieldRow = newInput.fieldRow; while (fieldIdx < fieldRow.length) { if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { @@ -210,9 +216,10 @@ export class ASTNode { const block = parentInput?.getSourceBlock(); if (!block || !parentInput) return null; - const curIdx = block.inputList.indexOf(parentInput); + const visibleInputs = this.getVisibleInputs(block); + const curIdx = visibleInputs.indexOf(parentInput); for (let i = curIdx; i >= 0; i--) { - const input = block.inputList[i]; + const input = visibleInputs[i]; if (input.connection && input !== parentInput) { return ASTNode.createInputNode(input); } @@ -242,10 +249,11 @@ export class ASTNode { 'The current AST location is not associated with a block', ); } - const curIdx = block.inputList.indexOf(parentInput); + const visibleInputs = this.getVisibleInputs(block); + const curIdx = visibleInputs.indexOf(parentInput); let fieldIdx = parentInput.fieldRow.indexOf(location) - 1; for (let i = curIdx; i >= 0; i--) { - const input = block.inputList[i]; + const input = visibleInputs[i]; if (input.connection && input !== parentInput) { return ASTNode.createInputNode(input); } @@ -259,7 +267,7 @@ export class ASTNode { // Reset the fieldIdx to the length of the field row of the previous // input. if (i - 1 >= 0) { - fieldIdx = block.inputList[i - 1].fieldRow.length - 1; + fieldIdx = visibleInputs[i - 1].fieldRow.length - 1; } } return null; @@ -458,10 +466,9 @@ export class ASTNode { * block. */ private findFirstFieldOrInput(block: Block): ASTNode | null { - const inputs = block.inputList; - for (let i = 0; i < inputs.length; i++) { - const input = inputs[i]; - if (!input.isVisible()) continue; + const visibleInputs = this.getVisibleInputs(block); + for (let i = 0; i < visibleInputs.length; i++) { + const input = visibleInputs[i]; const fieldRow = input.fieldRow; for (let j = 0; j < fieldRow.length; j++) { From dd3133baac1bff0caaacb845175f223e216c4935 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Fri, 2 May 2025 15:22:07 -0400 Subject: [PATCH 177/222] feat: add scope to keyboard shortcuts and use it (#8917) * feat: add scope to keyboard shortcuts * feat: add scope to keyboard shortcuts and use it --- core/shortcut_items.ts | 103 +++++++++++++------------- core/shortcut_registry.ts | 21 +++++- tests/mocha/keydown_test.js | 1 + tests/mocha/shortcut_registry_test.js | 89 ++++++++++++++++++++-- 4 files changed, 154 insertions(+), 60 deletions(-) diff --git a/core/shortcut_items.ts b/core/shortcut_items.ts index 0793e6213b4..afbea3ab285 100644 --- a/core/shortcut_items.ts +++ b/core/shortcut_items.ts @@ -8,12 +8,12 @@ import {BlockSvg} from './block_svg.js'; import * as clipboard from './clipboard.js'; -import * as common from './common.js'; import * as eventUtils from './events/utils.js'; import {Gesture} from './gesture.js'; import {ICopyData, isCopyable} from './interfaces/i_copyable.js'; import {isDeletable} from './interfaces/i_deletable.js'; import {isDraggable} from './interfaces/i_draggable.js'; +import {isSelectable} from './interfaces/i_selectable.js'; import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js'; import {Coordinate} from './utils/coordinate.js'; import {KeyCodes} from './utils/keycodes.js'; @@ -43,9 +43,7 @@ export function registerEscape() { return !workspace.isReadOnly(); }, callback(workspace) { - // AnyDuringMigration because: Property 'hideChaff' does not exist on - // type 'Workspace'. - (workspace as AnyDuringMigration).hideChaff(); + workspace.hideChaff(); return true; }, keyCodes: [KeyCodes.ESC], @@ -59,28 +57,28 @@ export function registerEscape() { export function registerDelete() { const deleteShortcut: KeyboardShortcut = { name: names.DELETE, - preconditionFn(workspace) { - const selected = common.getSelected(); + preconditionFn(workspace, scope) { + const focused = scope.focusedNode; return ( !workspace.isReadOnly() && - selected != null && - isDeletable(selected) && - selected.isDeletable() && + focused != null && + isDeletable(focused) && + focused.isDeletable() && !Gesture.inProgress() ); }, - callback(workspace, e) { + callback(workspace, e, shortcut, scope) { // Delete or backspace. // Stop the browser from going back to the previous page. // Do this first to prevent an error in the delete code from resulting in // data loss. e.preventDefault(); - const selected = common.getSelected(); - if (selected instanceof BlockSvg) { - selected.checkAndDelete(); - } else if (isDeletable(selected) && selected.isDeletable()) { + const focused = scope.focusedNode; + if (focused instanceof BlockSvg) { + focused.checkAndDelete(); + } else if (isDeletable(focused) && focused.isDeletable()) { eventUtils.setGroup(true); - selected.dispose(); + focused.dispose(); eventUtils.setGroup(false); } return true; @@ -110,33 +108,33 @@ export function registerCopy() { const copyShortcut: KeyboardShortcut = { name: names.COPY, - preconditionFn(workspace) { - const selected = common.getSelected(); + preconditionFn(workspace, scope) { + const focused = scope.focusedNode; return ( !workspace.isReadOnly() && !Gesture.inProgress() && - selected != null && - isDeletable(selected) && - selected.isDeletable() && - isDraggable(selected) && - selected.isMovable() && - isCopyable(selected) + focused != null && + isDeletable(focused) && + focused.isDeletable() && + isDraggable(focused) && + focused.isMovable() && + isCopyable(focused) ); }, - callback(workspace, e) { + callback(workspace, e, shortcut, scope) { // Prevent the default copy behavior, which may beep or otherwise indicate // an error due to the lack of a selection. e.preventDefault(); workspace.hideChaff(); - const selected = common.getSelected(); - if (!selected || !isCopyable(selected)) return false; - copyData = selected.toCopyData(); + const focused = scope.focusedNode; + if (!focused || !isCopyable(focused)) return false; + copyData = focused.toCopyData(); copyWorkspace = - selected.workspace instanceof WorkspaceSvg - ? selected.workspace + focused.workspace instanceof WorkspaceSvg + ? focused.workspace : workspace; - copyCoords = isDraggable(selected) - ? selected.getRelativeToSurfaceXY() + copyCoords = isDraggable(focused) + ? focused.getRelativeToSurfaceXY() : null; return !!copyData; }, @@ -161,39 +159,40 @@ export function registerCut() { const cutShortcut: KeyboardShortcut = { name: names.CUT, - preconditionFn(workspace) { - const selected = common.getSelected(); + preconditionFn(workspace, scope) { + const focused = scope.focusedNode; return ( !workspace.isReadOnly() && !Gesture.inProgress() && - selected != null && - isDeletable(selected) && - selected.isDeletable() && - isDraggable(selected) && - selected.isMovable() && - !selected.workspace!.isFlyout + focused != null && + isDeletable(focused) && + focused.isDeletable() && + isDraggable(focused) && + focused.isMovable() && + isSelectable(focused) && + !focused.workspace.isFlyout ); }, - callback(workspace) { - const selected = common.getSelected(); + callback(workspace, e, shortcut, scope) { + const focused = scope.focusedNode; - if (selected instanceof BlockSvg) { - copyData = selected.toCopyData(); + if (focused instanceof BlockSvg) { + copyData = focused.toCopyData(); copyWorkspace = workspace; - copyCoords = selected.getRelativeToSurfaceXY(); - selected.checkAndDelete(); + copyCoords = focused.getRelativeToSurfaceXY(); + focused.checkAndDelete(); return true; } else if ( - isDeletable(selected) && - selected.isDeletable() && - isCopyable(selected) + isDeletable(focused) && + focused.isDeletable() && + isCopyable(focused) ) { - copyData = selected.toCopyData(); + copyData = focused.toCopyData(); copyWorkspace = workspace; - copyCoords = isDraggable(selected) - ? selected.getRelativeToSurfaceXY() + copyCoords = isDraggable(focused) + ? focused.getRelativeToSurfaceXY() : null; - selected.dispose(); + focused.dispose(); return true; } return false; diff --git a/core/shortcut_registry.ts b/core/shortcut_registry.ts index 09bd867e769..b819cbbf748 100644 --- a/core/shortcut_registry.ts +++ b/core/shortcut_registry.ts @@ -12,6 +12,8 @@ */ // Former goog.module ID: Blockly.ShortcutRegistry +import {Scope} from './contextmenu_registry.js'; +import {getFocusManager} from './focus_manager.js'; import {KeyCodes} from './utils/keycodes.js'; import * as object from './utils/object.js'; import {WorkspaceSvg} from './workspace_svg.js'; @@ -249,12 +251,20 @@ export class ShortcutRegistry { const shortcut = this.shortcuts.get(shortcutName); if ( !shortcut || - (shortcut.preconditionFn && !shortcut.preconditionFn(workspace)) + (shortcut.preconditionFn && + !shortcut.preconditionFn(workspace, { + focusedNode: getFocusManager().getFocusedNode(), + })) ) { continue; } // If the key has been handled, stop processing shortcuts. - if (shortcut.callback?.(workspace, e, shortcut)) return true; + if ( + shortcut.callback?.(workspace, e, shortcut, { + focusedNode: getFocusManager().getFocusedNode(), + }) + ) + return true; } return false; } @@ -372,6 +382,8 @@ export namespace ShortcutRegistry { * @param e The event that caused the shortcut to be activated. * @param shortcut The `KeyboardShortcut` that was activated * (i.e., the one this callback is attached to). + * @param scope Information about the focused item when the + * shortcut was invoked. * @returns Returning true ends processing of the invoked keycode. * Returning false causes processing to continue with the * next-most-recently registered shortcut for the invoked @@ -381,6 +393,7 @@ export namespace ShortcutRegistry { workspace: WorkspaceSvg, e: Event, shortcut: KeyboardShortcut, + scope: Scope, ) => boolean; /** The name of the shortcut. Should be unique. */ @@ -393,9 +406,11 @@ export namespace ShortcutRegistry { * * @param workspace The `WorkspaceSvg` where the shortcut was * invoked. + * @param scope Information about the focused item when the + * shortcut would be invoked. * @returns True iff `callback` function should be called. */ - preconditionFn?: (workspace: WorkspaceSvg) => boolean; + preconditionFn?: (workspace: WorkspaceSvg, scope: Scope) => boolean; /** Optional arbitray extra data attached to the shortcut. */ metadata?: object; diff --git a/tests/mocha/keydown_test.js b/tests/mocha/keydown_test.js index 82293f22440..4ad0da2d4be 100644 --- a/tests/mocha/keydown_test.js +++ b/tests/mocha/keydown_test.js @@ -31,6 +31,7 @@ suite('Key Down', function () { defineStackBlock(); const block = workspace.newBlock('stack_block'); Blockly.common.setSelected(block); + sinon.stub(Blockly.getFocusManager(), 'getFocusedNode').returns(block); return block; } diff --git a/tests/mocha/shortcut_registry_test.js b/tests/mocha/shortcut_registry_test.js index 37c1b9c2023..9f412be3317 100644 --- a/tests/mocha/shortcut_registry_test.js +++ b/tests/mocha/shortcut_registry_test.js @@ -5,6 +5,7 @@ */ import {assert} from '../../node_modules/chai/chai.js'; +import {createTestBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, sharedTestTeardown, @@ -299,7 +300,7 @@ suite('Keyboard Shortcut Registry Test', function () { 'callback': function () { return true; }, - 'precondition': function () { + 'preconditionFn': function () { return true; }, }; @@ -319,6 +320,27 @@ suite('Keyboard Shortcut Registry Test', function () { const event = createKeyDownEvent(Blockly.utils.KeyCodes.D); assert.isFalse(this.registry.onKeyDown(this.workspace, event)); }); + test('No callback if precondition fails', function () { + const shortcut = { + 'name': 'test_shortcut', + 'callback': function () { + return true; + }, + 'preconditionFn': function () { + return false; + }, + }; + const callBackStub = addShortcut( + this.registry, + shortcut, + Blockly.utils.KeyCodes.C, + true, + ); + const event = createKeyDownEvent(Blockly.utils.KeyCodes.C); + assert.isFalse(this.registry.onKeyDown(this.workspace, event)); + sinon.assert.notCalled(callBackStub); + }); + test('No precondition available - execute callback', function () { delete this.testShortcut['precondition']; const event = createKeyDownEvent(Blockly.utils.KeyCodes.C); @@ -332,8 +354,8 @@ suite('Keyboard Shortcut Registry Test', function () { 'callback': function () { return false; }, - 'precondition': function () { - return false; + 'preconditionFn': function () { + return true; }, }; const testShortcut2Stub = addShortcut( @@ -353,8 +375,8 @@ suite('Keyboard Shortcut Registry Test', function () { 'callback': function () { return false; }, - 'precondition': function () { - return false; + 'preconditionFn': function () { + return true; }, }; const testShortcut2Stub = addShortcut( @@ -367,6 +389,63 @@ suite('Keyboard Shortcut Registry Test', function () { sinon.assert.calledOnce(testShortcut2Stub); sinon.assert.notCalled(this.callBackStub); }); + suite('interaction with FocusManager', function () { + setup(function () { + this.testShortcutWithScope = { + 'name': 'test_shortcut', + 'callback': function (workspace, e, shortcut, scope) { + return true; + }, + 'preconditionFn': function (workspace, scope) { + return true; + }, + }; + + // Stub the focus manager + this.focusedBlock = createTestBlock(); + sinon + .stub(Blockly.getFocusManager(), 'getFocusedNode') + .returns(this.focusedBlock); + }); + test('Callback receives the focused node', function () { + const event = createKeyDownEvent(Blockly.utils.KeyCodes.C); + const callbackStub = addShortcut( + this.registry, + this.testShortcutWithScope, + Blockly.utils.KeyCodes.C, + true, + ); + this.registry.onKeyDown(this.workspace, event); + + const expectedScope = {focusedNode: this.focusedBlock}; + sinon.assert.calledWithExactly( + callbackStub, + this.workspace, + event, + this.testShortcutWithScope, + expectedScope, + ); + }); + test('Precondition receives the focused node', function () { + const event = createKeyDownEvent(Blockly.utils.KeyCodes.C); + const callbackStub = addShortcut( + this.registry, + this.testShortcutWithScope, + Blockly.utils.KeyCodes.C, + true, + ); + const preconditionStub = sinon + .stub(this.testShortcutWithScope, 'preconditionFn') + .returns(true); + this.registry.onKeyDown(this.workspace, event); + const expectedScope = {focusedNode: this.focusedBlock}; + sinon.assert.calledWithExactly( + preconditionStub, + this.workspace, + expectedScope, + ); + }); + }); }); suite('createSerializedKey', function () { From a4e6166ca82342cd3382f8e15385505a22659e89 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Fri, 2 May 2025 12:23:07 -0700 Subject: [PATCH 178/222] fix!: Remove alt+key commands (#8961) * fix: Remove the alt+key commands * Remove tests for alt+key combos --- core/shortcut_items.ts | 26 +++++--------------------- tests/mocha/keydown_test.js | 19 ------------------- 2 files changed, 5 insertions(+), 40 deletions(-) diff --git a/core/shortcut_items.ts b/core/shortcut_items.ts index afbea3ab285..224678bc08e 100644 --- a/core/shortcut_items.ts +++ b/core/shortcut_items.ts @@ -99,9 +99,6 @@ export function registerCopy() { const ctrlC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [ KeyCodes.CTRL, ]); - const altC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [ - KeyCodes.ALT, - ]); const metaC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [ KeyCodes.META, ]); @@ -138,7 +135,7 @@ export function registerCopy() { : null; return !!copyData; }, - keyCodes: [ctrlC, altC, metaC], + keyCodes: [ctrlC, metaC], }; ShortcutRegistry.registry.register(copyShortcut); } @@ -150,9 +147,6 @@ export function registerCut() { const ctrlX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [ KeyCodes.CTRL, ]); - const altX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [ - KeyCodes.ALT, - ]); const metaX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [ KeyCodes.META, ]); @@ -197,7 +191,7 @@ export function registerCut() { } return false; }, - keyCodes: [ctrlX, altX, metaX], + keyCodes: [ctrlX, metaX], }; ShortcutRegistry.registry.register(cutShortcut); @@ -210,9 +204,6 @@ export function registerPaste() { const ctrlV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [ KeyCodes.CTRL, ]); - const altV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [ - KeyCodes.ALT, - ]); const metaV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [ KeyCodes.META, ]); @@ -245,7 +236,7 @@ export function registerPaste() { const centerCoords = new Coordinate(left + width / 2, top + height / 2); return !!clipboard.paste(copyData, copyWorkspace, centerCoords); }, - keyCodes: [ctrlV, altV, metaV], + keyCodes: [ctrlV, metaV], }; ShortcutRegistry.registry.register(pasteShortcut); @@ -258,9 +249,6 @@ export function registerUndo() { const ctrlZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ KeyCodes.CTRL, ]); - const altZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ - KeyCodes.ALT, - ]); const metaZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ KeyCodes.META, ]); @@ -277,7 +265,7 @@ export function registerUndo() { e.preventDefault(); return true; }, - keyCodes: [ctrlZ, altZ, metaZ], + keyCodes: [ctrlZ, metaZ], }; ShortcutRegistry.registry.register(undoShortcut); } @@ -291,10 +279,6 @@ export function registerRedo() { KeyCodes.SHIFT, KeyCodes.CTRL, ]); - const altShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ - KeyCodes.SHIFT, - KeyCodes.ALT, - ]); const metaShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ KeyCodes.SHIFT, KeyCodes.META, @@ -316,7 +300,7 @@ export function registerRedo() { e.preventDefault(); return true; }, - keyCodes: [ctrlShiftZ, altShiftZ, metaShiftZ, ctrlY], + keyCodes: [ctrlShiftZ, metaShiftZ, ctrlY], }; ShortcutRegistry.registry.register(redoShortcut); } diff --git a/tests/mocha/keydown_test.js b/tests/mocha/keydown_test.js index 4ad0da2d4be..a4c81a80c3b 100644 --- a/tests/mocha/keydown_test.js +++ b/tests/mocha/keydown_test.js @@ -132,12 +132,6 @@ suite('Key Down', function () { Blockly.utils.KeyCodes.META, ]), ], - [ - 'Alt C', - createKeyDownEvent(Blockly.utils.KeyCodes.C, [ - Blockly.utils.KeyCodes.ALT, - ]), - ], ]; // Copy a block. suite('Simple', function () { @@ -223,12 +217,6 @@ suite('Key Down', function () { Blockly.utils.KeyCodes.META, ]), ], - [ - 'Alt Z', - createKeyDownEvent(Blockly.utils.KeyCodes.Z, [ - Blockly.utils.KeyCodes.ALT, - ]), - ], ]; // Undo. suite('Simple', function () { @@ -289,13 +277,6 @@ suite('Key Down', function () { Blockly.utils.KeyCodes.SHIFT, ]), ], - [ - 'Alt Shift Z', - createKeyDownEvent(Blockly.utils.KeyCodes.Z, [ - Blockly.utils.KeyCodes.ALT, - Blockly.utils.KeyCodes.SHIFT, - ]), - ], ]; // Undo. suite('Simple', function () { From bc0e1c3ca316192ec79f39c503f12e2ed5f0a873 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Sat, 3 May 2025 00:21:41 +0100 Subject: [PATCH 179/222] feat(WorkspaceSvg): Ignore gestures when keyboard move in progress (#8963) * feat(WorkspaceSvg): Ignore gestures during keyboard moves Modify WorkspaceSvg.prototype.getGesture to return null when there is a keyboard-initiated move in progress. * chore(WorkspaceSvg): Add TODOs to remove .keyboardMoveInProgress --- core/workspace_svg.ts | 55 +++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 0ae0b5b6ef3..f9dc4638046 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -314,7 +314,7 @@ export class WorkspaceSvg keyboardAccessibilityMode = false; /** True iff a keyboard-initiated move ("drag") is in progress. */ - keyboardMoveInProgress = false; + keyboardMoveInProgress = false; // TODO(#8960): Delete this. /** The list of top-level bounded elements on the workspace. */ private topBoundedElements: IBoundedElement[] = []; @@ -1471,6 +1471,8 @@ export class WorkspaceSvg * removed, at an time without notice and without being treated * as a breaking change. * + * TODO(#8960): Delete this. + * * @internal * @param inProgress Is a keyboard-initated move in progress? */ @@ -1494,6 +1496,8 @@ export class WorkspaceSvg */ isDragging(): boolean { return ( + // TODO(#8960): Query Mover.isMoving to see if move is in + // progress rather than relying on a status flag. this.keyboardMoveInProgress || (this.currentGesture_ !== null && this.currentGesture_.isDragging()) ); @@ -2403,7 +2407,17 @@ export class WorkspaceSvg /** * Look up the gesture that is tracking this touch stream on this workspace. - * May create a new gesture. + * + * Returns the gesture in progress, except: + * + * - If there is a keyboard-initiate move in progress then null will + * be returned - after calling event.preventDefault() and + * event.stopPropagation() to ensure the pointer event is ignored. + * - If there is a gesture in progress but event.type is + * 'pointerdown' then the in-progress gesture will be cancelled; + * this will result in null being returned. + * - If no gesutre is in progress but event is a pointerdown then a + * new gesture will be created and returned. * * @param e Pointer event. * @returns The gesture that is tracking this touch stream, or null if no @@ -2411,28 +2425,29 @@ export class WorkspaceSvg * @internal */ getGesture(e: PointerEvent): Gesture | null { - const isStart = e.type === 'pointerdown'; - - const gesture = this.currentGesture_; - if (gesture) { - if (isStart && gesture.hasStarted()) { - console.warn('Tried to start the same gesture twice.'); - // That's funny. We must have missed a mouse up. - // Cancel it, rather than try to retrieve all of the state we need. - gesture.cancel(); - return null; - } - return gesture; + // TODO(#8960): Query Mover.isMoving to see if move is in progress + // rather than relying on .keyboardMoveInProgress status flag. + if (this.keyboardMoveInProgress) { + // Normally these would be called from Gesture.doStart. + e.preventDefault(); + e.stopPropagation(); + return null; } - // No gesture existed on this workspace, but this looks like the start of a - // new gesture. - if (isStart) { + const isStart = e.type === 'pointerdown'; + if (isStart && this.currentGesture_?.hasStarted()) { + console.warn('Tried to start the same gesture twice.'); + // That's funny. We must have missed a mouse up. + // Cancel it, rather than try to retrieve all of the state we need. + this.currentGesture_.cancel(); // Sets this.currentGesture_ to null. + } + if (!this.currentGesture_ && isStart) { + // No gesture existed on this workspace, but this looks like the + // start of a new gesture. this.currentGesture_ = new Gesture(e, this); - return this.currentGesture_; } - // No gesture existed and this event couldn't be the start of a new gesture. - return null; + + return this.currentGesture_; } /** From 9b6a79cf2ca3c25d332ed0cc888572615675e951 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 08:55:10 -0700 Subject: [PATCH 180/222] chore(deps): bump eslint from 9.24.0 to 9.26.0 (#8985) Bumps [eslint](https://github.com/eslint/eslint) from 9.24.0 to 9.26.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.24.0...v9.26.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 9.26.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 911 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 875 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4fbcf8f5bdb..7a61cd8a778 100644 --- a/package-lock.json +++ b/package-lock.json @@ -428,9 +428,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -478,9 +478,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", - "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", + "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", "dev": true, "license": "MIT", "engines": { @@ -511,19 +511,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@gulp-sourcemaps/identity-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", @@ -1131,6 +1118,28 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", + "integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1983,6 +1992,20 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -2780,6 +2803,27 @@ "ieee754": "^1.2.1" } }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -2839,6 +2883,37 @@ "node": ">= 0.8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3261,6 +3336,40 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -3279,6 +3388,26 @@ "safe-buffer": "~5.1.1" } }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/copy-props": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", @@ -3298,6 +3427,20 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/corser": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", @@ -3630,6 +3773,16 @@ "node": ">= 14" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3721,6 +3874,21 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/each-props": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", @@ -3809,12 +3977,29 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", @@ -3860,12 +4045,45 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", "dev": true }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es5-ext": { "version": "0.10.53", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", @@ -3919,6 +4137,13 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3965,23 +4190,24 @@ } }, "node_modules/eslint": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", - "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", + "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.24.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/js": "9.26.0", + "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", + "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -4005,7 +4231,8 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3" + "optionator": "^0.9.3", + "zod": "^3.24.2" }, "bin": { "eslint": "bin/eslint.js" @@ -4267,6 +4494,16 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -4301,6 +4538,29 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", + "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -4313,6 +4573,65 @@ "node": ">=0.10.0" } }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/ext": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", @@ -4526,6 +4845,24 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4689,6 +5026,26 @@ "node": ">=12.20.0" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -4817,17 +5174,56 @@ "node": "*" } }, - "node_modules/get-port": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", - "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, "engines": { - "node": ">=16" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/get-stream": { @@ -5117,6 +5513,19 @@ "win32" ] }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -5567,6 +5976,19 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -5638,6 +6060,23 @@ "entities": "^4.5.0" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -5847,6 +6286,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", @@ -6634,6 +7083,26 @@ "@ts-stack/markdown": "^1.3.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/memoizee": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", @@ -6656,6 +7125,19 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6692,6 +7174,29 @@ "node": ">=4" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6876,6 +7381,16 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/netmask": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", @@ -6992,6 +7507,19 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object.defaults": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", @@ -7019,6 +7547,19 @@ "node": ">=0.10.0" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7251,6 +7792,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/patch-package": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", @@ -7467,6 +8018,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/plugin-error": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", @@ -7630,6 +8191,20 @@ "node": ">=0.4.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-agent": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", @@ -7687,12 +8262,19 @@ } }, "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, "engines": { "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/query-selector-shadow-dom": { @@ -7737,6 +8319,32 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -8082,6 +8690,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -8192,6 +8834,29 @@ "node": ">= 10.13.0" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/serialize-error": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", @@ -8216,12 +8881,35 @@ "randombytes": "^2.1.0" } }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8252,6 +8940,82 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.1.tgz", @@ -8411,6 +9175,16 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stream-composer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", @@ -8811,6 +9585,16 @@ "node": ">=10.13.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -8902,6 +9686,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -9023,6 +9822,16 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -9087,6 +9896,16 @@ "node": ">= 10.13.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vinyl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", @@ -9713,6 +10532,26 @@ "dependencies": { "safe-buffer": "~5.2.0" } + }, + "node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } } } From c18c7ffef1f3f40e7059610ce6b0f80700294d5c Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 5 May 2025 09:27:12 -0700 Subject: [PATCH 181/222] fix: Fix Flyout auto-closing when creating a var. (#8982) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #8976 ### Proposed Changes Only auto-close the flyout if focus is being lost to a known tree. ### Reason for Changes I noticed from testing that the system does attempt to restore focus back to the flyout after creating a variable but the auto-closing logic was kicking in due to focus being lost with the variable creation prompt open. Even though an attempt was made to restore focus, this doesn't automatically reopen the flyout (since it is primarily governed by the toolbox selection state). One alternative might be to try and save the previously selected toolbox category and restore it, but that's tricky. This seems simpler, and also seems to largely maintain parity with pre-focus manager Blockly. Clicking outside of the toolbox with the flyout open only closes it if the click is within the toolbox itself or within the workspace. ### Test Coverage No new tests were added. However, it may be worth considering this specific case for future tests added with #8915. ### Documentation No new documentation seems necessary here. ### Additional Information None. --- core/workspace_svg.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index b791ceb79b1..8c3f014e24d 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2755,8 +2755,9 @@ export class WorkspaceSvg /** See IFocusableTree.onTreeBlur. */ onTreeBlur(nextTree: IFocusableTree | null): void { - // If the flyout loses focus, make sure to close it. - if (this.isFlyout && this.targetWorkspace) { + // If the flyout loses focus, make sure to close it unless focus is being + // lost to a different element on the page. + if (nextTree && this.isFlyout && this.targetWorkspace) { // Only hide the flyout if the flyout's workspace is losing focus and that // focus isn't returning to the flyout itself or the toolbox. const flyout = this.targetWorkspace.getFlyout(); From bbd97eab6770a5ca6019da2b6402d4656a285d4b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 5 May 2025 10:29:20 -0700 Subject: [PATCH 182/222] fix: Synchronize gestures and focus (#8981) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #8952 Fixes #8950 Fixes #8971 Fixes a bunch of other keyboard + mouse synchronization issues found during the testing discussed in https://github.com/google/blockly-keyboard-experimentation/pull/482#issuecomment-2846341307. ### Proposed Changes This introduces a number of changes to: - Ensure that gestures which change selection state also change focus. - Ensure that ephemeral focus is more robust against certain classes of failures. ### Reason for Changes There are some ephemeral focus issues that can come up with certain actions (like clicking or dragging) don't properly change focus. Beyond that, some users will likely mix clicking and keyboard navigation, so it's essential that focus and selection state stay in sync when switching between these two types of navigation modalities. Other changes: - Drop-down div was actually incorrectly releasing ephemeral focus before animated closes finish which could reset focus afterwards, breaking focus state. - Both drop-down and widget divs have been updated to only return focus _after_ marking the workspace as focused since the last focused node should always be the thing returned. - In a number of gesture cases selection has been removed. This is due to `BlockSvg` self-managing its selection state based on focus, so focusing is sufficient. I've also centralized some of the focusing calls (such as putting one in `bringBlockToFront` to ensure focusing happens after potential DOM changes). - Similarly, `BlockSvg`'s own `bringToFront` has been updated to automatically restore focus after the operation completes. Since `bringToFront` can always result in focus loss, this provides robustness to ensure focus is restored. - Block pasting now ensures that focus is properly set, however a delay is needed due to additional rendering changes that need to complete (I didn't dig deeply into the _why_ of this). - Dragging has been updated to always focus the moved block if it's not in the process of being deleted. - There was some selection resetting logic removed from gesture's `doWorkspaceClick` function. As far as I can tell, this won't be needed anymore since blocks self-regulate their selection state now. - `FocusManager` has a new extra check for ephemeral focus to catch a specific class of failure where the browser takes away focus immediately after it's returned. This can happen if there are delay timing situations (like animations) which result in a focused node being deleted (which then results in the document body receiving focus). The robustness check is possibly not needed, but it help discover the animation issue with drop-down div and shows some promise for helping to fix the variables-closing-flyout problem. Some caveats: - Some undo/redo steps can still result in focus being lost. This may introduce some regressions for selection state, and definitely introduces some annoyances with keyboard navigation. More work will be needed to understand how to better redirect focus (and to what) in cases when blocks disappear. - There are many other places where focus is being forced or selection state being overwritten that could, in theory, cause issues with focus state. These may need to be fixed in the future. - There's a lot of redundancy with `focusNode` being called more than it needs to be. `FocusManager` does avoid calling `focus()` more than once for the same node, but it's possible for focus state to switch between multiple nodes or elements even for a single gesture (for example, due to bringing the block to the front causing a DOM refresh). While the eventual consistency nature of the manager means this isn't a real problem, it may cause problems with screen reader output in the future and warrant another pass at reducing `focusNode` calls (particularly for gestures and the click event pipeline). ### Test Coverage This PR is largely relying on existing tests for regression verification, though no new tests have been added for the specific regression cases. #8910 is tracking improving `FocusManager` tests which could include some cases for the new ephemeral focus improvements. #8915 is tracking general accessibility testing which could include adding tests for these specific regression cases. ### Documentation No new documentation is expected to be needed here. ### Additional Information These changes originate from both #8875 and from a branch @rachel-fenichel created to investigate some of the failures this PR addresses. These changes have also been verified against both Core's playground and the keyboard navigation plugin's test environment. --- core/block_svg.ts | 9 ++++++++ core/clipboard/block_paster.ts | 12 ++++++++-- core/dragging/dragger.ts | 8 +++++++ core/dropdowndiv.ts | 13 +++++------ core/focus_manager.ts | 40 +++++++++++++++++++++++++++++----- core/gesture.ts | 33 ++++++++++++++++++---------- core/widgetdiv.ts | 9 ++++---- 7 files changed, 94 insertions(+), 30 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 3d7d285c263..362accf17e9 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -34,6 +34,7 @@ import type {BlockMove} from './events/events_block_move.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import {FieldLabel} from './field_label.js'; +import {getFocusManager} from './focus_manager.js'; import {IconType} from './icons/icon_types.js'; import {MutatorIcon} from './icons/mutator_icon.js'; import {WarningIcon} from './icons/warning_icon.js'; @@ -1290,6 +1291,7 @@ export class BlockSvg * adjusting its parents. */ bringToFront(blockOnly = false) { + const previouslyFocused = getFocusManager().getFocusedNode(); /* eslint-disable-next-line @typescript-eslint/no-this-alias */ let block: this | null = this; if (block.isDeadOrDying()) { @@ -1306,6 +1308,13 @@ export class BlockSvg if (blockOnly) break; block = block.getParent(); } while (block); + if (previouslyFocused) { + // Bringing a block to the front of the stack doesn't fundamentally change + // the logical structure of the page, but it does change element ordering + // which can take automatically take away focus from a node. Ensure focus + // is restored to avoid a discontinuity. + getFocusManager().focusNode(previouslyFocused); + } } /** diff --git a/core/clipboard/block_paster.ts b/core/clipboard/block_paster.ts index 08ff220ee91..750cedca124 100644 --- a/core/clipboard/block_paster.ts +++ b/core/clipboard/block_paster.ts @@ -5,12 +5,14 @@ */ import {BlockSvg} from '../block_svg.js'; -import * as common from '../common.js'; +import {IFocusableNode} from '../blockly.js'; import {config} from '../config.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; +import {getFocusManager} from '../focus_manager.js'; import {ICopyData} from '../interfaces/i_copyable.js'; import {IPaster} from '../interfaces/i_paster.js'; +import * as renderManagement from '../render_management.js'; import {State, append} from '../serialization/blocks.js'; import {Coordinate} from '../utils/coordinate.js'; import {WorkspaceSvg} from '../workspace_svg.js'; @@ -55,7 +57,13 @@ export class BlockPaster implements IPaster { if (eventUtils.isEnabled() && !block.isShadow()) { eventUtils.fire(new (eventUtils.get(EventType.BLOCK_CREATE))(block)); } - common.setSelected(block); + + // Sometimes there's a delay before the block is fully created and ready for + // focusing, so wait slightly before focusing the newly pasted block. + const nodeToFocus: IFocusableNode = block; + renderManagement + .finishQueuedRenders() + .then(() => getFocusManager().focusNode(nodeToFocus)); return block; } } diff --git a/core/dragging/dragger.ts b/core/dragging/dragger.ts index 518351d5c86..02e9e2bfb79 100644 --- a/core/dragging/dragger.ts +++ b/core/dragging/dragger.ts @@ -8,11 +8,13 @@ import * as blockAnimations from '../block_animations.js'; import {BlockSvg} from '../block_svg.js'; import {ComponentManager} from '../component_manager.js'; import * as eventUtils from '../events/utils.js'; +import {getFocusManager} from '../focus_manager.js'; import {IDeletable, isDeletable} from '../interfaces/i_deletable.js'; import {IDeleteArea} from '../interfaces/i_delete_area.js'; import {IDragTarget} from '../interfaces/i_drag_target.js'; import {IDraggable} from '../interfaces/i_draggable.js'; import {IDragger} from '../interfaces/i_dragger.js'; +import {isFocusableNode} from '../interfaces/i_focusable_node.js'; import * as registry from '../registry.js'; import {Coordinate} from '../utils/coordinate.js'; import {WorkspaceSvg} from '../workspace_svg.js'; @@ -129,6 +131,12 @@ export class Dragger implements IDragger { root.dispose(); } eventUtils.setGroup(false); + + if (!wouldDelete && isFocusableNode(this.draggable)) { + // Ensure focusable nodes that have finished dragging (but aren't being + // deleted) end with focus and selection. + getFocusManager().focusNode(this.draggable); + } } // We need to special case blocks for now so that we look at the root block diff --git a/core/dropdowndiv.ts b/core/dropdowndiv.ts index dcf8fa24ef7..e326ac94cb4 100644 --- a/core/dropdowndiv.ts +++ b/core/dropdowndiv.ts @@ -629,10 +629,6 @@ export function hide() { animateOutTimer = setTimeout(function () { hideWithoutAnimation(); }, ANIMATION_TIME * 1000); - if (returnEphemeralFocus) { - returnEphemeralFocus(); - returnEphemeralFocus = null; - } if (onHide) { onHide(); onHide = null; @@ -648,10 +644,6 @@ export function hideWithoutAnimation() { clearTimeout(animateOutTimer); } - if (returnEphemeralFocus) { - returnEphemeralFocus(); - returnEphemeralFocus = null; - } if (onHide) { onHide(); onHide = null; @@ -660,6 +652,11 @@ export function hideWithoutAnimation() { owner = null; (common.getMainWorkspace() as WorkspaceSvg).markFocused(); + + if (returnEphemeralFocus) { + returnEphemeralFocus(); + returnEphemeralFocus = null; + } } /** diff --git a/core/focus_manager.ts b/core/focus_manager.ts index 7091c4efb08..f9a62afecbb 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -56,17 +56,20 @@ export class FocusManager { */ static readonly PASSIVE_FOCUS_NODE_CSS_CLASS_NAME = 'blocklyPassiveFocus'; - focusedNode: IFocusableNode | null = null; - registeredTrees: Array = []; + private focusedNode: IFocusableNode | null = null; + private previouslyFocusedNode: IFocusableNode | null = null; + private registeredTrees: Array = []; private currentlyHoldsEphemeralFocus: boolean = false; private lockFocusStateChanges: boolean = false; + private recentlyLostAllFocus: boolean = false; constructor( addGlobalEventListener: (type: string, listener: EventListener) => void, ) { // Note that 'element' here is the element *gaining* focus. const maybeFocus = (element: Element | EventTarget | null) => { + this.recentlyLostAllFocus = !element; let newNode: IFocusableNode | null | undefined = null; if (element instanceof HTMLElement || element instanceof SVGElement) { // If the target losing or gaining focus maps to any tree, then it @@ -164,7 +167,7 @@ export class FocusManager { const root = tree.getRootFocusableNode(); if (focusedNode) this.removeHighlight(focusedNode); if (this.focusedNode === focusedNode || this.focusedNode === root) { - this.focusedNode = null; + this.updateFocusedNode(null); } this.removeHighlight(root); } @@ -277,7 +280,7 @@ export class FocusManager { // Only change the actively focused node if ephemeral state isn't held. this.activelyFocusNode(focusableNode, prevTree ?? null); } - this.focusedNode = focusableNode; + this.updateFocusedNode(focusableNode); } /** @@ -328,6 +331,22 @@ export class FocusManager { if (this.focusedNode) { this.activelyFocusNode(this.focusedNode, null); + + // Even though focus was restored, check if it's lost again. It's + // possible for the browser to force focus away from all elements once + // the ephemeral element disappears. This ensures focus is restored. + const capturedNode = this.focusedNode; + setTimeout(() => { + // These checks are set up to minimize the risk that a legitimate + // focus change occurred within the delay that this would override. + if ( + !this.focusedNode && + this.previouslyFocusedNode === capturedNode && + this.recentlyLostAllFocus + ) { + this.focusNode(capturedNode); + } + }, 0); } }; } @@ -348,6 +367,17 @@ export class FocusManager { } } + /** + * Updates the internally tracked focused node to the specified node, or null + * if focus is being lost. This also updates previous focus tracking. + * + * @param newFocusedNode The new node to set as focused. + */ + private updateFocusedNode(newFocusedNode: IFocusableNode | null) { + this.previouslyFocusedNode = this.focusedNode; + this.focusedNode = newFocusedNode; + } + /** * Defocuses the current actively focused node tracked by the manager, iff * there's a node being tracked and the manager doesn't have ephemeral focus. @@ -358,7 +388,7 @@ export class FocusManager { // restored upon exiting ephemeral focus mode. if (this.focusedNode && !this.currentlyHoldsEphemeralFocus) { this.passivelyFocusNode(this.focusedNode, null); - this.focusedNode = null; + this.updateFocusedNode(null); } } diff --git a/core/gesture.ts b/core/gesture.ts index fc23ba7ca15..f9b435c67d9 100644 --- a/core/gesture.ts +++ b/core/gesture.ts @@ -25,6 +25,7 @@ import * as dropDownDiv from './dropdowndiv.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import type {Field} from './field.js'; +import {getFocusManager} from './focus_manager.js'; import type {IBubble} from './interfaces/i_bubble.js'; import {IDraggable, isDraggable} from './interfaces/i_draggable.js'; import {IDragger} from './interfaces/i_dragger.js'; @@ -289,7 +290,7 @@ export class Gesture { // The start block is no longer relevant, because this is a drag. this.startBlock = null; this.targetBlock = this.flyout.createBlock(this.targetBlock); - common.setSelected(this.targetBlock); + getFocusManager().focusNode(this.targetBlock); return true; } return false; @@ -734,6 +735,7 @@ export class Gesture { this.startComment.showContextMenu(e); } else if (this.startWorkspace_ && !this.flyout) { this.startWorkspace_.hideChaff(); + getFocusManager().focusNode(this.startWorkspace_); this.startWorkspace_.showContextMenu(e); } @@ -762,9 +764,12 @@ export class Gesture { this.mostRecentEvent = e; if (!this.startBlock && !this.startBubble && !this.startComment) { - // Selection determines what things start drags. So to drag the workspace, - // we need to deselect anything that was previously selected. - common.setSelected(null); + // Ensure the workspace is selected if nothing else should be. Note that + // this is focusNode() instead of focusTree() because if any active node + // is focused in the workspace it should be defocused. + getFocusManager().focusNode(ws); + } else if (this.startBlock) { + getFocusManager().focusNode(this.startBlock); } this.doStart(e); @@ -865,13 +870,18 @@ export class Gesture { ); } + // Note that the order is important here: bringing a block to the front will + // cause it to become focused and showing the field editor will capture + // focus ephemerally. It's important to ensure that focus is properly + // restored back to the block after field editing has completed. + this.bringBlockToFront(); + // Only show the editor if the field's editor wasn't already open // right before this gesture started. const dropdownAlreadyOpen = this.currentDropdownOwner === this.startField; if (!dropdownAlreadyOpen) { this.startField.showEditor(this.mostRecentEvent); } - this.bringBlockToFront(); } /** Execute an icon click. */ @@ -901,6 +911,9 @@ export class Gesture { const newBlock = this.flyout.createBlock(this.targetBlock); newBlock.snapToGrid(); newBlock.bumpNeighbours(); + + // If a new block was added, make sure that it's correctly focused. + getFocusManager().focusNode(newBlock); } } else { if (!this.startWorkspace_) { @@ -928,11 +941,7 @@ export class Gesture { * @param _e A pointerup event. */ private doWorkspaceClick(_e: PointerEvent) { - const ws = this.creatorWorkspace; - if (common.getSelected()) { - common.getSelected()!.unselect(); - } - this.fireWorkspaceClick(this.startWorkspace_ || ws); + this.fireWorkspaceClick(this.startWorkspace_ || this.creatorWorkspace); } /* End functions defining what actions to take to execute clicks on each type @@ -947,6 +956,8 @@ export class Gesture { private bringBlockToFront() { // Blocks in the flyout don't overlap, so skip the work. if (this.targetBlock && !this.flyout) { + // Always ensure the block being dragged/clicked has focus. + getFocusManager().focusNode(this.targetBlock); this.targetBlock.bringToFront(); } } @@ -1023,7 +1034,6 @@ export class Gesture { // If the gesture already went through a bubble, don't set the start block. if (!this.startBlock && !this.startBubble) { this.startBlock = block; - common.setSelected(this.startBlock); if (block.isInFlyout && block !== block.getRootBlock()) { this.setTargetBlock(block.getRootBlock()); } else { @@ -1046,6 +1056,7 @@ export class Gesture { this.setTargetBlock(block.getParent()!); } else { this.targetBlock = block; + getFocusManager().focusNode(block); } } diff --git a/core/widgetdiv.ts b/core/widgetdiv.ts index cb006160455..608927b6fb0 100644 --- a/core/widgetdiv.ts +++ b/core/widgetdiv.ts @@ -131,10 +131,6 @@ export function hide() { div.style.display = 'none'; div.style.left = ''; div.style.top = ''; - if (returnEphemeralFocus) { - returnEphemeralFocus(); - returnEphemeralFocus = null; - } if (dispose) { dispose(); dispose = null; @@ -150,6 +146,11 @@ export function hide() { themeClassName = ''; } (common.getMainWorkspace() as WorkspaceSvg).markFocused(); + + if (returnEphemeralFocus) { + returnEphemeralFocus(); + returnEphemeralFocus = null; + } } /** From 233604a74af65df40dba68da8b50f2d0122d6fa2 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Mon, 5 May 2025 15:30:33 -0400 Subject: [PATCH 183/222] fix: focus for autohideable flyouts (#8990) --- core/interfaces/i_autohideable.ts | 5 +++++ core/workspace_svg.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/interfaces/i_autohideable.ts b/core/interfaces/i_autohideable.ts index ecdec8595a6..41e761f57ca 100644 --- a/core/interfaces/i_autohideable.ts +++ b/core/interfaces/i_autohideable.ts @@ -20,3 +20,8 @@ export interface IAutoHideable extends IComponent { */ autoHide(onlyClosePopups: boolean): void; } + +/** Returns true if the given object is autohideable. */ +export function isAutoHideable(obj: any): obj is IAutoHideable { + return obj.autoHide !== undefined; +} diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 8c3f014e24d..43a8416cd74 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -41,6 +41,7 @@ import {getFocusManager} from './focus_manager.js'; import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; +import {isAutoHideable} from './interfaces/i_autohideable.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {IDragTarget} from './interfaces/i_drag_target.js'; @@ -2765,7 +2766,7 @@ export class WorkspaceSvg if (flyout && nextTree === flyout) return; if (toolbox && nextTree === toolbox) return; if (toolbox) toolbox.clearSelection(); - if (flyout && flyout instanceof Flyout) flyout.autoHide(false); + if (flyout && isAutoHideable(flyout)) flyout.autoHide(false); } } } From e21d37da003cf7c33134a25abc7d9279a57bc2da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 10:27:06 -0700 Subject: [PATCH 184/222] chore(deps): bump typescript-eslint from 8.23.0 to 8.31.1 (#8984) Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.23.0 to 8.31.1. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.31.1/packages/typescript-eslint) --- updated-dependencies: - dependency-name: typescript-eslint dependency-version: 8.31.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 114 +++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7a61cd8a778..6cc798fb482 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1523,17 +1523,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", - "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", + "integrity": "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/type-utils": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/type-utils": "8.31.1", + "@typescript-eslint/utils": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1549,20 +1549,20 @@ "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", - "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz", + "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/typescript-estree": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", "debug": "^4.3.4" }, "engines": { @@ -1574,18 +1574,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", - "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz", + "integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0" + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1596,14 +1596,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", - "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz", + "integrity": "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/utils": "8.23.0", + "@typescript-eslint/typescript-estree": "8.31.1", + "@typescript-eslint/utils": "8.31.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1616,13 +1616,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz", + "integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==", "dev": true, "license": "MIT", "engines": { @@ -1634,14 +1634,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz", + "integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1657,7 +1657,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -1700,16 +1700,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", - "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.1.tgz", + "integrity": "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0" + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/typescript-estree": "8.31.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1720,17 +1720,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz", + "integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/types": "8.31.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -9629,9 +9629,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { @@ -9722,15 +9722,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.23.0.tgz", - "integrity": "sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz", + "integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.23.0", - "@typescript-eslint/parser": "8.23.0", - "@typescript-eslint/utils": "8.23.0" + "@typescript-eslint/eslint-plugin": "8.31.1", + "@typescript-eslint/parser": "8.31.1", + "@typescript-eslint/utils": "8.31.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9741,7 +9741,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/unc-path-regex": { From 04a31f950f0ce19f53c62608bb2e30f6c7287f05 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Tue, 6 May 2025 10:58:05 -0700 Subject: [PATCH 185/222] fix: Wrap toolbox refreshes in a timeout when modifying variables (#8980) --- blocks/variables.ts | 2 -- blocks/variables_dynamic.ts | 2 -- core/field_variable.ts | 4 ---- core/variable_map.ts | 19 ------------------- core/workspace_svg.ts | 21 +++++++++++++++++++++ 5 files changed, 21 insertions(+), 27 deletions(-) diff --git a/blocks/variables.ts b/blocks/variables.ts index 4651c43ec29..4f1f640fa81 100644 --- a/blocks/variables.ts +++ b/blocks/variables.ts @@ -21,7 +21,6 @@ import '../core/field_label.js'; import {FieldVariable} from '../core/field_variable.js'; import {Msg} from '../core/msg.js'; import * as Variables from '../core/variables.js'; -import type {WorkspaceSvg} from '../core/workspace_svg.js'; /** * A dictionary of the block definitions provided by this module. @@ -170,7 +169,6 @@ const deleteOptionCallbackFactory = function ( if (variable) { Variables.deleteVariable(variable.getWorkspace(), variable, block); } - (block.workspace as WorkspaceSvg).refreshToolboxSelection(); }; }; diff --git a/blocks/variables_dynamic.ts b/blocks/variables_dynamic.ts index a6e78ef4a34..8afd24cf2e3 100644 --- a/blocks/variables_dynamic.ts +++ b/blocks/variables_dynamic.ts @@ -22,7 +22,6 @@ import '../core/field_label.js'; import {FieldVariable} from '../core/field_variable.js'; import {Msg} from '../core/msg.js'; import * as Variables from '../core/variables.js'; -import type {WorkspaceSvg} from '../core/workspace_svg.js'; /** * A dictionary of the block definitions provided by this module. @@ -181,7 +180,6 @@ const deleteOptionCallbackFactory = function (block: VariableBlock) { if (variable) { Variables.deleteVariable(variable.getWorkspace(), variable, block); } - (block.workspace as WorkspaceSvg).refreshToolboxSelection(); }; }; diff --git a/core/field_variable.ts b/core/field_variable.ts index 83a235597a5..b54b113cd69 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -32,7 +32,6 @@ import * as dom from './utils/dom.js'; import * as parsing from './utils/parsing.js'; import {Size} from './utils/size.js'; import * as Variables from './variables.js'; -import {WorkspaceSvg} from './workspace_svg.js'; import * as Xml from './xml.js'; /** @@ -523,9 +522,6 @@ export class FieldVariable extends FieldDropdown { // Delete variable. const workspace = this.variable.getWorkspace(); Variables.deleteVariable(workspace, this.variable, this.sourceBlock_); - if (workspace instanceof WorkspaceSvg) { - workspace.refreshToolboxSelection(); - } return; } } diff --git a/core/variable_map.ts b/core/variable_map.ts index 6195034775e..44386d2f7d4 100644 --- a/core/variable_map.ts +++ b/core/variable_map.ts @@ -27,7 +27,6 @@ import * as deprecation from './utils/deprecation.js'; import * as idGenerator from './utils/idgenerator.js'; import {deleteVariable, getVariableUsesById} from './variables.js'; import type {Workspace} from './workspace.js'; -import {WorkspaceSvg} from './workspace_svg.js'; /** * Class for a variable map. This contains a dictionary data structure with @@ -93,9 +92,6 @@ export class VariableMap } finally { eventUtils.setGroup(existingGroup); } - if (this.workspace instanceof WorkspaceSvg) { - this.workspace.refreshToolboxSelection(); - } return variable; } @@ -112,9 +108,6 @@ export class VariableMap if (!this.variableMap.has(newType)) { this.variableMap.set(newType, newTypeVariables); } - if (this.workspace instanceof WorkspaceSvg) { - this.workspace.refreshToolboxSelection(); - } return variable; } @@ -161,9 +154,6 @@ export class VariableMap for (let i = 0; i < blocks.length; i++) { blocks[i].updateVarName(variable); } - if (this.workspace instanceof WorkspaceSvg) { - this.workspace.refreshToolboxSelection(); - } } /** @@ -259,9 +249,6 @@ export class VariableMap this.variableMap.set(type, variables); } eventUtils.fire(new (eventUtils.get(EventType.VAR_CREATE))(variable)); - if (this.workspace instanceof WorkspaceSvg) { - this.workspace.refreshToolboxSelection(); - } return variable; } @@ -279,9 +266,6 @@ export class VariableMap ); } this.variableMap.get(type)?.set(variable.getId(), variable); - if (this.workspace instanceof WorkspaceSvg) { - this.workspace.refreshToolboxSelection(); - } } /* Begin functions for variable deletion. */ @@ -310,9 +294,6 @@ export class VariableMap } finally { eventUtils.setGroup(existingGroup); } - if (this.workspace instanceof WorkspaceSvg) { - this.workspace.refreshToolboxSelection(); - } } /** diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 43a8416cd74..1892ec171ae 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -33,6 +33,7 @@ import { ContextMenuRegistry, } from './contextmenu_registry.js'; import * as dropDownDiv from './dropdowndiv.js'; +import {Abstract as AbstractEvent} from './events/events.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import {Flyout} from './flyout_base.js'; @@ -399,6 +400,9 @@ export class WorkspaceSvg this.addChangeListener(Procedures.mutatorOpenListener); } + // Set up callbacks to refresh the toolbox when variables change + this.addChangeListener(this.variableChangeCallback.bind(this)); + /** Object in charge of storing and updating the workspace theme. */ this.themeManager_ = this.options.parentWorkspace ? this.options.parentWorkspace.getThemeManager() @@ -1361,6 +1365,23 @@ export class WorkspaceSvg } } + /** + * Handles any necessary updates when a variable changes. + * + * @internal + */ + variableChangeCallback(event: AbstractEvent) { + switch (event.type) { + case EventType.VAR_CREATE: + case EventType.VAR_DELETE: + case EventType.VAR_RENAME: + case EventType.VAR_TYPE_CHANGE: + this.refreshToolboxSelection(); + break; + default: + } + } + /** * Refresh the toolbox unless there's a drag in progress. * From eb5181e3ef015d4e7e3bd5ac2eeb7a40860478fd Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Tue, 6 May 2025 11:13:25 -0700 Subject: [PATCH 186/222] fix: Add private to variableChangeCallback (#8995) --- core/workspace_svg.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 1892ec171ae..e67634e272b 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -1370,7 +1370,7 @@ export class WorkspaceSvg * * @internal */ - variableChangeCallback(event: AbstractEvent) { + private variableChangeCallback(event: AbstractEvent) { switch (event.type) { case EventType.VAR_CREATE: case EventType.VAR_DELETE: From 86c831a3fef72e2537e72d064acd585d2cdcc3a9 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Tue, 6 May 2025 14:37:28 -0400 Subject: [PATCH 187/222] fix: use copyable interface for cut action, add tests (#8993) * fix: use copyable for cut action * chore: rename keydown test * chore: add tests for shortcut items not acting on focused connections --- core/shortcut_items.ts | 3 +- tests/mocha/index.html | 2 +- ...keydown_test.js => shortcut_items_test.js} | 44 ++++++++++++++++++- 3 files changed, 44 insertions(+), 5 deletions(-) rename tests/mocha/{keydown_test.js => shortcut_items_test.js} (87%) diff --git a/core/shortcut_items.ts b/core/shortcut_items.ts index 224678bc08e..a16e22aa33b 100644 --- a/core/shortcut_items.ts +++ b/core/shortcut_items.ts @@ -13,7 +13,6 @@ import {Gesture} from './gesture.js'; import {ICopyData, isCopyable} from './interfaces/i_copyable.js'; import {isDeletable} from './interfaces/i_deletable.js'; import {isDraggable} from './interfaces/i_draggable.js'; -import {isSelectable} from './interfaces/i_selectable.js'; import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js'; import {Coordinate} from './utils/coordinate.js'; import {KeyCodes} from './utils/keycodes.js'; @@ -163,7 +162,7 @@ export function registerCut() { focused.isDeletable() && isDraggable(focused) && focused.isMovable() && - isSelectable(focused) && + isCopyable(focused) && !focused.workspace.isFlyout ); }, diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 1c9f1fbbc6a..2b99102f645 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -242,7 +242,6 @@ import './jso_deserialization_test.js'; import './jso_serialization_test.js'; import './json_test.js'; - import './keydown_test.js'; import './layering_test.js'; import './blocks/lists_test.js'; import './blocks/logic_ternary_test.js'; @@ -258,6 +257,7 @@ import './registry_test.js'; import './render_management_test.js'; import './serializer_test.js'; + import './shortcut_items_test.js'; import './shortcut_registry_test.js'; import './touch_test.js'; import './theme_test.js'; diff --git a/tests/mocha/keydown_test.js b/tests/mocha/shortcut_items_test.js similarity index 87% rename from tests/mocha/keydown_test.js rename to tests/mocha/shortcut_items_test.js index a4c81a80c3b..29487ec5822 100644 --- a/tests/mocha/keydown_test.js +++ b/tests/mocha/shortcut_items_test.js @@ -12,7 +12,7 @@ import { } from './test_helpers/setup_teardown.js'; import {createKeyDownEvent} from './test_helpers/user_input.js'; -suite('Key Down', function () { +suite('Keyboard Shortcut Items', function () { setup(function () { sharedTestSetup.call(this); this.workspace = Blockly.inject('blocklyDiv', {}); @@ -35,6 +35,18 @@ suite('Key Down', function () { return block; } + /** + * Creates a block and sets its nextConnection as the focused node. + * @param {Blockly.Workspace} workspace The workspace to create a new block on. + */ + function setSelectedConnection(workspace) { + defineStackBlock(); + const block = workspace.newBlock('stack_block'); + sinon + .stub(Blockly.getFocusManager(), 'getFocusedNode') + .returns(block.nextConnection); + } + /** * Creates a test for not running keyDown events when the workspace is in read only mode. * @param {Object} keyEvent Mocked key down event. Use createKeyDownEvent. @@ -73,9 +85,14 @@ suite('Key Down', function () { this.injectionDiv.dispatchEvent(this.event); sinon.assert.notCalled(this.hideChaffSpy); }); + test('Called when connection is focused', function () { + setSelectedConnection(this.workspace); + this.injectionDiv.dispatchEvent(this.event); + sinon.assert.calledOnce(this.hideChaffSpy); + }); }); - suite('Delete Block', function () { + suite('Delete', function () { setup(function () { this.hideChaffSpy = sinon.spy( Blockly.WorkspaceSvg.prototype, @@ -89,6 +106,7 @@ suite('Key Down', function () { ['Backspace', createKeyDownEvent(Blockly.utils.KeyCodes.BACKSPACE)], ]; // Delete a block. + // Note that chaff is hidden when a block is deleted. suite('Simple', function () { testCases.forEach(function (testCase) { const testCaseName = testCase[0]; @@ -108,6 +126,16 @@ suite('Key Down', function () { runReadOnlyTest(keyEvent, testCaseName); }); }); + // Do not delete anything if a connection is focused. + test('Not called when connection is focused', function () { + // Restore the stub behavior called during setup + Blockly.getFocusManager().getFocusedNode.restore(); + + setSelectedConnection(this.workspace); + const event = createKeyDownEvent(Blockly.utils.KeyCodes.DELETE); + this.injectionDiv.dispatchEvent(event); + sinon.assert.notCalled(this.hideChaffSpy); + }); }); suite('Copy', function () { @@ -194,6 +222,18 @@ suite('Key Down', function () { }); }); }); + test('Not called when connection is focused', function () { + // Restore the stub behavior called during setup + Blockly.getFocusManager().getFocusedNode.restore(); + + setSelectedConnection(this.workspace); + const event = createKeyDownEvent(Blockly.utils.KeyCodes.C, [ + Blockly.utils.KeyCodes.CTRL, + ]); + this.injectionDiv.dispatchEvent(event); + sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); }); suite('Undo', function () { From a3b3ea72f2da49f02336261773c3854eb7b53a5b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 6 May 2025 12:57:19 -0700 Subject: [PATCH 188/222] fix: Improve missing node resiliency (#8997) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #8994 ### Proposed Changes This removes an error that was previously thrown by `FocusManager` when attempting to focus an invalid node (such as one that's been removed from its parent). ### Reason for Changes https://github.com/google/blockly/issues/8994#issuecomment-2855447539 goes into more detail. While this error did cover legitimately wrong cases to try and focus things (and helped to catch some real problems), fixing this 'properly' may become a leaky boat problem where we have to track down every possible asynchronous scenario that could produce such a case. One class of this is ephemeral focus which had robustness improvements itself in #8981 that, by effect, caused this issue in the first place. Holistically fixing this with enforced API contracts alone isn't simple due to the nature of how these components interact. This change ensures that there's a sane default to fall back on if an invalid node is passed in. Note that `FocusManager` was designed specifically to disallow defocusing a node (since fallbacks can get messy and introduce unpredictable user experiences), and this sort of allows that now. However, this seems like a reasonable approach as it defaults to the behavior when focusing a tree explicitly which allows the tree to fallback to a more suitable default (such as the first item to select in the toolbox for that particular tree). In many cases this will default back to the tree's root node (such as the workspace root group) since sometimes the removed node is still the "last focused node" of the tree (and is considered valid for the purpose of determining a fallback; tree implementations could further specialize by checking whether that node is still valid). ### Test Coverage Some new tests were added to cover this case, but more may be useful to add as part of #8910. ### Documentation No documentation needs to be added or updated as part of this (beyond code documentation changes). ### Additional Information This original issue was found by @RoboErikG when testing #8995. I also verified this against the keyboard navigation plugin repository. --- core/focus_manager.ts | 26 ++++++++++++----------- core/interfaces/i_focusable_tree.ts | 4 ++++ tests/mocha/focus_manager_test.js | 32 ++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/core/focus_manager.ts b/core/focus_manager.ts index f9a62afecbb..4c5fab16043 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -227,7 +227,7 @@ export class FocusManager { } /** - * Focuses DOM input on the selected node, and marks it as actively focused. + * Focuses DOM input on the specified node, and marks it as actively focused. * * Any previously focused node will be updated to be passively highlighted (if * it's in a different focusable tree) or blurred (if it's in the same one). @@ -244,17 +244,20 @@ export class FocusManager { } // Safety check for ensuring focusNode() doesn't get called for a node that - // isn't actually hooked up to its parent tree correctly (since this can - // cause weird inconsistencies). + // isn't actually hooked up to its parent tree correctly. This usually + // happens when calls to focusNode() interleave with asynchronous clean-up + // operations (which can happen due to ephemeral focus and in other cases). + // Fall back to a reasonable default since there's no valid node to focus. const matchedNode = FocusableTreeTraverser.findFocusableNodeFor( focusableNode.getFocusableElement(), nextTree, ); + const prevNodeNextTree = FocusableTreeTraverser.findFocusedNode(nextTree); + let nodeToFocus = focusableNode; if (matchedNode !== focusableNode) { - throw Error( - `Attempting to focus node which isn't recognized by its parent tree: ` + - `${focusableNode}.`, - ); + const nodeToRestore = nextTree.getRestoredFocusableNode(prevNodeNextTree); + const rootFallback = nextTree.getRootFocusableNode(); + nodeToFocus = nodeToRestore ?? prevNodeNextTree ?? rootFallback; } const prevNode = this.focusedNode; @@ -264,7 +267,6 @@ export class FocusManager { } // If there's a focused node in the new node's tree, ensure it's reset. - const prevNodeNextTree = FocusableTreeTraverser.findFocusedNode(nextTree); const nextTreeRoot = nextTree.getRootFocusableNode(); if (prevNodeNextTree) { this.removeHighlight(prevNodeNextTree); @@ -272,19 +274,19 @@ export class FocusManager { // For caution, ensure that the root is always reset since getFocusedNode() // is expected to return null if the root was highlighted, if the root is // not the node now being set to active. - if (nextTreeRoot !== focusableNode) { + if (nextTreeRoot !== nodeToFocus) { this.removeHighlight(nextTreeRoot); } if (!this.currentlyHoldsEphemeralFocus) { // Only change the actively focused node if ephemeral state isn't held. - this.activelyFocusNode(focusableNode, prevTree ?? null); + this.activelyFocusNode(nodeToFocus, prevTree ?? null); } - this.updateFocusedNode(focusableNode); + this.updateFocusedNode(nodeToFocus); } /** - * Ephemerally captures focus for a selected element until the returned lambda + * Ephemerally captures focus for a specific element until the returned lambda * is called. This is expected to be especially useful for ephemeral UI flows * like dialogs. * diff --git a/core/interfaces/i_focusable_tree.ts b/core/interfaces/i_focusable_tree.ts index 69afa24ffdf..f4f25f7f518 100644 --- a/core/interfaces/i_focusable_tree.ts +++ b/core/interfaces/i_focusable_tree.ts @@ -52,6 +52,10 @@ export interface IFocusableTree { * bypass this method. * 3. The default behavior (i.e. returning null here) involves either * restoring the previous node (previousNode) or focusing the tree's root. + * 4. The provided node may sometimes no longer be valid, such as in the case + * an attempt is made to focus a node that has been recently removed from + * its parent tree. Implementations can check for the validity of the node + * in order to specialize the node to which focus should fall back. * * This method is largely intended to provide tree implementations with the * means of specifying a better default node than their root. diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js index bcd74c1a03c..264d91734c1 100644 --- a/tests/mocha/focus_manager_test.js +++ b/tests/mocha/focus_manager_test.js @@ -38,6 +38,7 @@ class FocusableTreeImpl { this.nestedTrees = nestedTrees; this.idToNodeMap = {}; this.rootNode = this.addNode(rootElement); + this.fallbackNode = null; } addNode(element) { @@ -46,12 +47,16 @@ class FocusableTreeImpl { return node; } + removeNode(node) { + delete this.idToNodeMap[node.getFocusableElement().id]; + } + getRootFocusableNode() { return this.rootNode; } getRestoredFocusableNode() { - return null; + return this.fallbackNode; } getNestedTrees() { @@ -385,6 +390,31 @@ suite('FocusManager', function () { // There should be exactly 1 focus event fired from focusNode(). assert.strictEqual(focusCount, 1); }); + + test('for orphaned node returns tree root by default', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.testFocusableTree1.removeNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + // Focusing an invalid node should fall back to the tree root when it has no restoration + // fallback node. + const currentNode = this.focusManager.getFocusedNode(); + const treeRoot = this.testFocusableTree1.getRootFocusableNode(); + assert.strictEqual(currentNode, treeRoot); + }); + + test('for orphaned node returns specified fallback node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.testFocusableTree1.fallbackNode = this.testFocusableTree1Node2; + this.testFocusableTree1.removeNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + // Focusing an invalid node should fall back to the restored fallback. + const currentNode = this.focusManager.getFocusedNode(); + assert.strictEqual(currentNode, this.testFocusableTree1Node2); + }); }); suite('getFocusManager()', function () { From acdad9865373ccf19d12a4f210d2fb42d398cea5 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 7 May 2025 08:47:52 -0700 Subject: [PATCH 189/222] refactor!: Use navigation rulesets instead of ASTNode to control keyboard navigation. (#8992) * feat: Add interfaces for keyboard navigation. * feat: Add the Navigator. * feat: Make core types conform to INavigable. * feat: Require FlyoutItems elements to be INavigable. * feat: Add navigation policies for built-in types. * refactor: Convert Marker and LineCursor to operate on INavigables instead of ASTNodes. * chore: Delete dead code in ASTNode. * fix: Fix the tests. * chore: Assuage the linter. * fix: Fix advanced build/tests. * chore: Restore ASTNode tests. * refactor: Move isNavigable() validation into Navigator. * refactor: Exercise navigation instead of ASTNode. * chore: Rename astnode_test.js to navigation_test.js. * chore: Enable the navigation tests. * fix: Fix bug when retrieving the first child of an empty workspace. --- core/block_flyout_inflater.ts | 2 +- core/block_svg.ts | 25 +- core/blockly.ts | 10 + core/button_flyout_inflater.ts | 2 +- core/field.ts | 28 +- core/field_checkbox.ts | 12 + core/field_dropdown.ts | 12 + core/field_image.ts | 12 + core/field_label.ts | 12 + core/field_number.ts | 12 + core/field_textinput.ts | 12 + core/flyout_base.ts | 1 - core/flyout_button.ts | 26 +- core/flyout_item.ts | 14 +- core/flyout_separator.ts | 27 +- core/interfaces/i_navigable.ts | 31 + core/interfaces/i_navigation_policy.ts | 46 + core/keyboard_nav/ast_node.ts | 537 ----------- core/keyboard_nav/block_navigation_policy.ts | 117 +++ .../connection_navigation_policy.ts | 169 ++++ core/keyboard_nav/field_navigation_policy.ts | 91 ++ .../flyout_button_navigation_policy.ts | 56 ++ core/keyboard_nav/flyout_navigation_policy.ts | 91 ++ .../flyout_separator_navigation_policy.ts | 33 + core/keyboard_nav/line_cursor.ts | 357 ++++---- core/keyboard_nav/marker.ts | 62 +- .../workspace_navigation_policy.ts | 66 ++ core/label_flyout_inflater.ts | 2 +- core/navigator.ts | 106 +++ core/rendered_connection.ts | 21 +- core/separator_flyout_inflater.ts | 2 +- core/workspace_svg.ts | 43 +- tests/mocha/astnode_test.js | 850 ------------------ tests/mocha/cursor_test.js | 189 ++-- tests/mocha/index.html | 2 +- tests/mocha/navigation_test.js | 567 ++++++++++++ .../src/field/different_user_input.ts | 4 + 37 files changed, 1941 insertions(+), 1708 deletions(-) create mode 100644 core/interfaces/i_navigable.ts create mode 100644 core/interfaces/i_navigation_policy.ts create mode 100644 core/keyboard_nav/block_navigation_policy.ts create mode 100644 core/keyboard_nav/connection_navigation_policy.ts create mode 100644 core/keyboard_nav/field_navigation_policy.ts create mode 100644 core/keyboard_nav/flyout_button_navigation_policy.ts create mode 100644 core/keyboard_nav/flyout_navigation_policy.ts create mode 100644 core/keyboard_nav/flyout_separator_navigation_policy.ts create mode 100644 core/keyboard_nav/workspace_navigation_policy.ts create mode 100644 core/navigator.ts delete mode 100644 tests/mocha/astnode_test.js create mode 100644 tests/mocha/navigation_test.js diff --git a/core/block_flyout_inflater.ts b/core/block_flyout_inflater.ts index 49f65c1f38e..80f86855182 100644 --- a/core/block_flyout_inflater.ts +++ b/core/block_flyout_inflater.ts @@ -70,7 +70,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater { block.getDescendants(false).forEach((b) => (b.isInFlyout = true)); this.addBlockListeners(block); - return new FlyoutItem(block, BLOCK_TYPE, true); + return new FlyoutItem(block, BLOCK_TYPE); } /** diff --git a/core/block_svg.ts b/core/block_svg.ts index 362accf17e9..06cb7e1a233 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -48,6 +48,7 @@ import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {IIcon} from './interfaces/i_icon.js'; +import type {INavigable} from './interfaces/i_navigable.js'; import * as internalConstants from './internal_constants.js'; import {MarkerManager} from './marker_manager.js'; import {Msg} from './msg.js'; @@ -80,7 +81,8 @@ export class BlockSvg ICopyable, IDraggable, IDeletable, - IFocusableNode + IFocusableNode, + INavigable { /** * Constant for identifying rows that are to be rendered inline. @@ -1886,4 +1888,25 @@ export class BlockSvg common.setSelected(null); } } + + /** + * Returns whether or not this block can be navigated to via the keyboard. + * + * @returns True if this block is keyboard navigable, otherwise false. + */ + isNavigable() { + return true; + } + + /** + * Returns this block's class. + * + * Used by keyboard navigation to look up the rules for navigating from this + * block. + * + * @returns This block's class. + */ + getClass() { + return BlockSvg; + } } diff --git a/core/blockly.ts b/core/blockly.ts index 069e21c81f6..682649673c6 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -433,6 +433,16 @@ Names.prototype.populateProcedures = function ( }; // clang-format on +export * from './interfaces/i_navigable.js'; +export * from './interfaces/i_navigation_policy.js'; +export * from './keyboard_nav/block_navigation_policy.js'; +export * from './keyboard_nav/connection_navigation_policy.js'; +export * from './keyboard_nav/field_navigation_policy.js'; +export * from './keyboard_nav/flyout_button_navigation_policy.js'; +export * from './keyboard_nav/flyout_navigation_policy.js'; +export * from './keyboard_nav/flyout_separator_navigation_policy.js'; +export * from './keyboard_nav/workspace_navigation_policy.js'; +export * from './navigator.js'; export * from './toast.js'; // Re-export submodules that no longer declareLegacyNamespace. diff --git a/core/button_flyout_inflater.ts b/core/button_flyout_inflater.ts index 665ce7a2425..4f083f015f7 100644 --- a/core/button_flyout_inflater.ts +++ b/core/button_flyout_inflater.ts @@ -33,7 +33,7 @@ export class ButtonFlyoutInflater implements IFlyoutInflater { ); button.show(); - return new FlyoutItem(button, BUTTON_TYPE, true); + return new FlyoutItem(button, BUTTON_TYPE); } /** diff --git a/core/field.ts b/core/field.ts index a5e43a27665..c89a53de51e 100644 --- a/core/field.ts +++ b/core/field.ts @@ -28,6 +28,7 @@ import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_w import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js'; +import type {INavigable} from './interfaces/i_navigable.js'; import type {IRegistrable} from './interfaces/i_registrable.js'; import {ISerializable} from './interfaces/i_serializable.js'; import {MarkerManager} from './marker_manager.js'; @@ -76,7 +77,8 @@ export abstract class Field IKeyboardAccessible, IRegistrable, ISerializable, - IFocusableNode + IFocusableNode, + INavigable> { /** * To overwrite the default value which is set in **Field**, directly update @@ -1452,6 +1454,30 @@ export abstract class Field `Attempted to instantiate a field from the registry that hasn't defined a 'fromJson' method.`, ); } + + /** + * Returns whether or not this field is accessible by keyboard navigation. + * + * @returns True if this field is keyboard accessible, otherwise false. + */ + isNavigable() { + return ( + this.isClickable() && + this.isCurrentlyEditable() && + !(this.getSourceBlock()?.isSimpleReporter() && this.isFullBlockField()) && + this.getParentInput().isVisible() + ); + } + + /** + * Returns this field's class. + * + * Used by keyboard navigation to look up the rules for navigating from this + * field. Must be implemented by subclasses. + * + * @returns This field's class. + */ + abstract getClass(): new (...args: any) => Field; } /** diff --git a/core/field_checkbox.ts b/core/field_checkbox.ts index 55ed42cbf4b..85c176bc3ae 100644 --- a/core/field_checkbox.ts +++ b/core/field_checkbox.ts @@ -228,6 +228,18 @@ export class FieldCheckbox extends Field { // 'override' the static fromJson method. return new this(options.checked, undefined, options); } + + /** + * Returns this field's class. + * + * Used by keyboard navigation to look up the rules for navigating from this + * field. + * + * @returns This field's class. + */ + getClass() { + return FieldCheckbox; + } } fieldRegistry.register('field_checkbox', FieldCheckbox); diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 0b787bd7701..38c5727f2b6 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -796,6 +796,18 @@ export class FieldDropdown extends Field { throw TypeError('Found invalid FieldDropdown options.'); } } + + /** + * Returns this field's class. + * + * Used by keyboard navigation to look up the rules for navigating from this + * field. + * + * @returns This field's class. + */ + getClass() { + return FieldDropdown; + } } /** diff --git a/core/field_image.ts b/core/field_image.ts index 650575f59a3..b65b77ae29c 100644 --- a/core/field_image.ts +++ b/core/field_image.ts @@ -273,6 +273,18 @@ export class FieldImage extends Field { options, ); } + + /** + * Returns this field's class. + * + * Used by keyboard navigation to look up the rules for navigating from this + * field. + * + * @returns This field's class. + */ + getClass() { + return FieldImage; + } } fieldRegistry.register('field_image', FieldImage); diff --git a/core/field_label.ts b/core/field_label.ts index 236154cc7b1..6b8437d2b95 100644 --- a/core/field_label.ts +++ b/core/field_label.ts @@ -126,6 +126,18 @@ export class FieldLabel extends Field { // the static fromJson method. return new this(text, undefined, options); } + + /** + * Returns this field's class. + * + * Used by keyboard navigation to look up the rules for navigating from this + * field. + * + * @returns This field's class. + */ + getClass() { + return FieldLabel; + } } fieldRegistry.register('field_label', FieldLabel); diff --git a/core/field_number.ts b/core/field_number.ts index 7e36591753e..6ecda1011be 100644 --- a/core/field_number.ts +++ b/core/field_number.ts @@ -341,6 +341,18 @@ export class FieldNumber extends FieldInput { options, ); } + + /** + * Returns this field's class. + * + * Used by keyboard navigation to look up the rules for navigating from this + * field. + * + * @returns This field's class. + */ + getClass() { + return FieldNumber; + } } fieldRegistry.register('field_number', FieldNumber); diff --git a/core/field_textinput.ts b/core/field_textinput.ts index 2b896ad47be..6ec2331d023 100644 --- a/core/field_textinput.ts +++ b/core/field_textinput.ts @@ -89,6 +89,18 @@ export class FieldTextInput extends FieldInput { // override the static fromJson method. return new this(text, undefined, options); } + + /** + * Returns this field's class. + * + * Used by keyboard navigation to look up the rules for navigating from this + * field. + * + * @returns This field's class. + */ + getClass() { + return FieldTextInput; + } } fieldRegistry.register('field_input', FieldTextInput); diff --git a/core/flyout_base.ts b/core/flyout_base.ts index a0ba75eb9fe..82ae0c23d4d 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -696,7 +696,6 @@ export abstract class Flyout this.horizontalLayout ? SeparatorAxis.X : SeparatorAxis.Y, ), SEPARATOR_TYPE, - false, ), ); } diff --git a/core/flyout_button.ts b/core/flyout_button.ts index 666a2e081bf..2b314ed2ebc 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -17,6 +17,7 @@ import * as Css from './css.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; +import type {INavigable} from './interfaces/i_navigable.js'; import type {IRenderedElement} from './interfaces/i_rendered_element.js'; import {idGenerator} from './utils.js'; import {Coordinate} from './utils/coordinate.js'; @@ -36,7 +37,8 @@ export class FlyoutButton IASTNodeLocationSvg, IBoundedElement, IRenderedElement, - IFocusableNode + IFocusableNode, + INavigable { /** The horizontal margin around the text in the button. */ static TEXT_MARGIN_X = 5; @@ -416,6 +418,28 @@ export class FlyoutButton /** See IFocusableNode.onNodeBlur. */ onNodeBlur(): void {} + + /** + * Returns whether or not this button is accessible through keyboard + * navigation. + * + * @returns True if this button is keyboard accessible, otherwise false. + */ + isNavigable() { + return true; + } + + /** + * Returns this button's class. + * + * Used by keyboard navigation to look up the rules for navigating from this + * button. + * + * @returns This button's class. + */ + getClass() { + return FlyoutButton; + } } /** CSS for buttons and labels. See css.js for use. */ diff --git a/core/flyout_item.ts b/core/flyout_item.ts index d501ceedbbf..ab973cb97e4 100644 --- a/core/flyout_item.ts +++ b/core/flyout_item.ts @@ -1,5 +1,5 @@ import type {IBoundedElement} from './interfaces/i_bounded_element.js'; - +import type {INavigable} from './interfaces/i_navigable.js'; /** * Representation of an item displayed in a flyout. */ @@ -10,13 +10,10 @@ export class FlyoutItem { * @param element The element that will be displayed in the flyout. * @param type The type of element. Should correspond to the type of the * flyout inflater that created this object. - * @param focusable True if the element should be allowed to be focused by - * e.g. keyboard navigation in the flyout. */ constructor( - private element: IBoundedElement, + private element: IBoundedElement & INavigable, private type: string, - private focusable: boolean, ) {} /** @@ -32,11 +29,4 @@ export class FlyoutItem { getType() { return this.type; } - - /** - * Returns whether or not the flyout element can receive focus. - */ - isFocusable() { - return this.focusable; - } } diff --git a/core/flyout_separator.ts b/core/flyout_separator.ts index 733371007a1..02737879a7c 100644 --- a/core/flyout_separator.ts +++ b/core/flyout_separator.ts @@ -5,12 +5,15 @@ */ import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {INavigable} from './interfaces/i_navigable.js'; import {Rect} from './utils/rect.js'; /** * Representation of a gap between elements in a flyout. */ -export class FlyoutSeparator implements IBoundedElement { +export class FlyoutSeparator + implements IBoundedElement, INavigable +{ private x = 0; private y = 0; @@ -50,6 +53,28 @@ export class FlyoutSeparator implements IBoundedElement { this.x += dx; this.y += dy; } + + /** + * Returns false to prevent this separator from being navigated to by the + * keyboard. + * + * @returns False. + */ + isNavigable() { + return false; + } + + /** + * Returns this separator's class. + * + * Used by keyboard navigation to look up the rules for navigating from this + * separator. + * + * @returns This separator's class. + */ + getClass() { + return FlyoutSeparator; + } } /** diff --git a/core/interfaces/i_navigable.ts b/core/interfaces/i_navigable.ts new file mode 100644 index 00000000000..f41e7882934 --- /dev/null +++ b/core/interfaces/i_navigable.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Represents a UI element which can be navigated to using the keyboard. + */ +export interface INavigable { + /** + * Returns whether or not this specific instance should be reachable via + * keyboard navigation. + * + * Implementors should generally return true, unless there are circumstances + * under which this item should be skipped while using keyboard navigation. + * Common examples might include being disabled, invalid, readonly, or purely + * a visual decoration. For example, while Fields are navigable, non-editable + * fields return false, since they cannot be interacted with when focused. + * + * @returns True if this element should be included in keyboard navigation. + */ + isNavigable(): boolean; + + /** + * Returns the class of this instance. + * + * @returns This object's class. + */ + getClass(): new (...args: any) => T; +} diff --git a/core/interfaces/i_navigation_policy.ts b/core/interfaces/i_navigation_policy.ts new file mode 100644 index 00000000000..b263aac5987 --- /dev/null +++ b/core/interfaces/i_navigation_policy.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {INavigable} from './i_navigable.js'; + +/** + * A set of rules that specify where keyboard navigation should proceed. + */ +export interface INavigationPolicy { + /** + * Returns the first child element of the given element, if any. + * + * @param current The element which the user is navigating into. + * @returns The current element's first child, or null if it has none. + */ + getFirstChild(current: T): INavigable | null; + + /** + * Returns the parent element of the given element, if any. + * + * @param current The element which the user is navigating out of. + * @returns The parent element of the current element, or null if it has none. + */ + getParent(current: T): INavigable | null; + + /** + * Returns the peer element following the given element, if any. + * + * @param current The element which the user is navigating past. + * @returns The next peer element of the current element, or null if there is + * none. + */ + getNextSibling(current: T): INavigable | null; + + /** + * Returns the peer element preceding the given element, if any. + * + * @param current The element which the user is navigating past. + * @returns The previous peer element of the current element, or null if + * there is none. + */ + getPreviousSibling(current: T): INavigable | null; +} diff --git a/core/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts index 6844b87f954..ea58fd0ab5f 100644 --- a/core/keyboard_nav/ast_node.ts +++ b/core/keyboard_nav/ast_node.ts @@ -13,18 +13,15 @@ // Former goog.module ID: Blockly.ASTNode import {Block} from '../block.js'; -import {BlockSvg} from '../block_svg.js'; import type {Connection} from '../connection.js'; import {ConnectionType} from '../connection_type.js'; import type {Field} from '../field.js'; import {FlyoutButton} from '../flyout_button.js'; -import type {FlyoutItem} from '../flyout_item.js'; import type {Input} from '../inputs/input.js'; import type {IASTNodeLocation} from '../interfaces/i_ast_node_location.js'; import type {IASTNodeLocationWithBlock} from '../interfaces/i_ast_node_location_with_block.js'; import {Coordinate} from '../utils/coordinate.js'; import type {Workspace} from '../workspace.js'; -import {WorkspaceSvg} from '../workspace_svg.js'; /** * Class for an AST node. @@ -135,355 +132,6 @@ export class ASTNode { return block.inputList.filter((input) => input.isVisible()); } - /** - * Given an input find the next editable field or an input with a non null - * connection in the same block. The current location must be an input - * connection. - * - * @returns The AST node holding the next field or connection or null if there - * is no editable field or input connection after the given input. - */ - private findNextForInput(): ASTNode | null { - const location = this.location as Connection; - const parentInput = location.getParentInput(); - const block = parentInput?.getSourceBlock(); - if (!block || !parentInput) return null; - - const visibleInputs = this.getVisibleInputs(block); - const curIdx = visibleInputs.indexOf(parentInput); - for (let i = curIdx + 1; i < visibleInputs.length; i++) { - const input = visibleInputs[i]; - const fieldRow = input.fieldRow; - for (let j = 0; j < fieldRow.length; j++) { - const field = fieldRow[j]; - if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { - return ASTNode.createFieldNode(field); - } - } - if (input.connection) { - return ASTNode.createInputNode(input); - } - } - return null; - } - - /** - * Given a field find the next editable field or an input with a non null - * connection in the same block. The current location must be a field. - * - * @returns The AST node pointing to the next field or connection or null if - * there is no editable field or input connection after the given input. - */ - private findNextForField(): ASTNode | null { - const location = this.location as Field; - const input = location.getParentInput(); - const block = location.getSourceBlock(); - if (!block) { - throw new Error( - 'The current AST location is not associated with a block', - ); - } - const visibleInputs = this.getVisibleInputs(block); - const curIdx = visibleInputs.indexOf(input); - let fieldIdx = input.fieldRow.indexOf(location) + 1; - for (let i = curIdx; i < visibleInputs.length; i++) { - const newInput = visibleInputs[i]; - const fieldRow = newInput.fieldRow; - while (fieldIdx < fieldRow.length) { - if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { - return ASTNode.createFieldNode(fieldRow[fieldIdx]); - } - fieldIdx++; - } - fieldIdx = 0; - if (newInput.connection) { - return ASTNode.createInputNode(newInput); - } - } - return null; - } - - /** - * Given an input find the previous editable field or an input with a non null - * connection in the same block. The current location must be an input - * connection. - * - * @returns The AST node holding the previous field or connection. - */ - private findPrevForInput(): ASTNode | null { - const location = this.location as Connection; - const parentInput = location.getParentInput(); - const block = parentInput?.getSourceBlock(); - if (!block || !parentInput) return null; - - const visibleInputs = this.getVisibleInputs(block); - const curIdx = visibleInputs.indexOf(parentInput); - for (let i = curIdx; i >= 0; i--) { - const input = visibleInputs[i]; - if (input.connection && input !== parentInput) { - return ASTNode.createInputNode(input); - } - const fieldRow = input.fieldRow; - for (let j = fieldRow.length - 1; j >= 0; j--) { - const field = fieldRow[j]; - if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { - return ASTNode.createFieldNode(field); - } - } - } - return null; - } - - /** - * Given a field find the previous editable field or an input with a non null - * connection in the same block. The current location must be a field. - * - * @returns The AST node holding the previous input or field. - */ - private findPrevForField(): ASTNode | null { - const location = this.location as Field; - const parentInput = location.getParentInput(); - const block = location.getSourceBlock(); - if (!block) { - throw new Error( - 'The current AST location is not associated with a block', - ); - } - const visibleInputs = this.getVisibleInputs(block); - const curIdx = visibleInputs.indexOf(parentInput); - let fieldIdx = parentInput.fieldRow.indexOf(location) - 1; - for (let i = curIdx; i >= 0; i--) { - const input = visibleInputs[i]; - if (input.connection && input !== parentInput) { - return ASTNode.createInputNode(input); - } - const fieldRow = input.fieldRow; - while (fieldIdx > -1) { - if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { - return ASTNode.createFieldNode(fieldRow[fieldIdx]); - } - fieldIdx--; - } - // Reset the fieldIdx to the length of the field row of the previous - // input. - if (i - 1 >= 0) { - fieldIdx = visibleInputs[i - 1].fieldRow.length - 1; - } - } - return null; - } - - /** - * Navigate between stacks of blocks on the workspace. - * - * @param forward True to go forward. False to go backwards. - * @returns The first block of the next stack or null if there are no blocks - * on the workspace. - */ - private navigateBetweenStacks(forward: boolean): ASTNode | null { - let curLocation = this.getLocation(); - // TODO(#6097): Use instanceof checks to exit early for values of - // curLocation that don't make sense. - if ((curLocation as IASTNodeLocationWithBlock).getSourceBlock) { - const block = (curLocation as IASTNodeLocationWithBlock).getSourceBlock(); - if (block) { - curLocation = block; - } - } - // TODO(#6097): Use instanceof checks to exit early for values of - // curLocation that don't make sense. - const curLocationAsBlock = curLocation as Block; - if (!curLocationAsBlock || curLocationAsBlock.isDeadOrDying()) { - return null; - } - if (curLocationAsBlock.workspace.isFlyout) { - return this.navigateFlyoutContents(forward); - } - const curRoot = curLocationAsBlock.getRootBlock(); - const topBlocks = curRoot.workspace.getTopBlocks(true); - for (let i = 0; i < topBlocks.length; i++) { - const topBlock = topBlocks[i]; - if (curRoot.id === topBlock.id) { - const offset = forward ? 1 : -1; - const resultIndex = i + offset; - if (resultIndex === -1 || resultIndex === topBlocks.length) { - return null; - } - return ASTNode.createStackNode(topBlocks[resultIndex]); - } - } - throw Error( - "Couldn't find " + (forward ? 'next' : 'previous') + ' stack?!', - ); - } - - /** - * Navigate between buttons and stacks of blocks on the flyout workspace. - * - * @param forward True to go forward. False to go backwards. - * @returns The next button, or next stack's first block, or null - */ - private navigateFlyoutContents(forward: boolean): ASTNode | null { - const nodeType = this.getType(); - let location; - let targetWorkspace; - - switch (nodeType) { - case ASTNode.types.STACK: { - location = this.getLocation() as Block; - const workspace = location.workspace as WorkspaceSvg; - targetWorkspace = workspace.targetWorkspace as WorkspaceSvg; - break; - } - case ASTNode.types.BUTTON: { - location = this.getLocation() as FlyoutButton; - targetWorkspace = location.getTargetWorkspace() as WorkspaceSvg; - break; - } - default: - return null; - } - - const flyout = targetWorkspace.getFlyout(); - if (!flyout) return null; - - const nextItem = this.findNextLocationInFlyout( - flyout.getContents(), - location, - forward, - ); - if (!nextItem) return null; - - const element = nextItem.getElement(); - if (element instanceof FlyoutButton) { - return ASTNode.createButtonNode(element); - } else if (element instanceof BlockSvg) { - return ASTNode.createStackNode(element); - } - - return null; - } - - /** - * Finds the next (or previous if navigating backward) item in the flyout that should be navigated to. - * - * @param flyoutContents Contents of the current flyout. - * @param currentLocation Current ASTNode location. - * @param forward True if we're navigating forward, else false. - * @returns The next (or previous) FlyoutItem, or null if there is none. - */ - private findNextLocationInFlyout( - flyoutContents: FlyoutItem[], - currentLocation: IASTNodeLocation, - forward: boolean, - ): FlyoutItem | null { - const currentIndex = flyoutContents.findIndex((item: FlyoutItem) => { - if ( - currentLocation instanceof BlockSvg && - item.getElement() === currentLocation - ) { - return true; - } - if ( - currentLocation instanceof FlyoutButton && - item.getElement() === currentLocation - ) { - return true; - } - return false; - }); - - if (currentIndex < 0) return null; - - let resultIndex = forward ? currentIndex + 1 : currentIndex - 1; - // The flyout may contain non-focusable elements like spacers or custom - // items. If the next/previous element is one of those, keep looking until a - // focusable element is encountered. - while ( - resultIndex >= 0 && - resultIndex < flyoutContents.length && - !flyoutContents[resultIndex].isFocusable() - ) { - resultIndex += forward ? 1 : -1; - } - if (resultIndex === -1 || resultIndex === flyoutContents.length) { - return null; - } - - return flyoutContents[resultIndex]; - } - - /** - * Finds the top most AST node for a given block. - * This is either the previous connection, output connection or block - * depending on what kind of connections the block has. - * - * @param block The block that we want to find the top connection on. - * @returns The AST node containing the top connection. - */ - private findTopASTNodeForBlock(block: Block): ASTNode | null { - const topConnection = getParentConnection(block); - if (topConnection) { - return ASTNode.createConnectionNode(topConnection); - } else { - return ASTNode.createBlockNode(block); - } - } - - /** - * Get the AST node pointing to the input that the block is nested under or if - * the block is not nested then get the stack AST node. - * - * @param block The source block of the current location. - * @returns The AST node pointing to the input connection or the top block of - * the stack this block is in. - */ - private getOutAstNodeForBlock(block: Block): ASTNode | null { - if (!block) { - return null; - } - // If the block doesn't have a previous connection then it is the top of the - // substack. - const topBlock = block.getTopStackBlock(); - const topConnection = getParentConnection(topBlock); - const parentInput = topConnection?.targetConnection?.getParentInput(); - // If the top connection has a parentInput, create an AST node pointing to - // that input. - if (parentInput) { - return ASTNode.createInputNode(parentInput); - } else { - // Go to stack level if you are not underneath an input. - return ASTNode.createStackNode(topBlock); - } - } - - /** - * Find the first editable field or input with a connection on a given block. - * - * @param block The source block of the current location. - * @returns An AST node pointing to the first field or input. - * Null if there are no editable fields or inputs with connections on the - * block. - */ - private findFirstFieldOrInput(block: Block): ASTNode | null { - const visibleInputs = this.getVisibleInputs(block); - for (let i = 0; i < visibleInputs.length; i++) { - const input = visibleInputs[i]; - - const fieldRow = input.fieldRow; - for (let j = 0; j < fieldRow.length; j++) { - const field = fieldRow[j]; - if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { - return ASTNode.createFieldNode(field); - } - } - if (input.connection) { - return ASTNode.createInputNode(input); - } - } - return null; - } - /** * Finds the source block of the location of this node. * @@ -504,191 +152,6 @@ export class ASTNode { } } - /** - * Find the element to the right of the current element in the AST. - * - * @returns An AST node that wraps the next field, connection, block, or - * workspace. Or null if there is no node to the right. - */ - next(): ASTNode | null { - switch (this.type) { - case ASTNode.types.STACK: - return this.navigateBetweenStacks(true); - - case ASTNode.types.OUTPUT: { - const connection = this.location as Connection; - return ASTNode.createBlockNode(connection.getSourceBlock()); - } - case ASTNode.types.FIELD: - return this.findNextForField(); - - case ASTNode.types.INPUT: - return this.findNextForInput(); - - case ASTNode.types.BLOCK: { - const block = this.location as Block; - const nextConnection = block.nextConnection; - if (!nextConnection) return null; - return ASTNode.createConnectionNode(nextConnection); - } - case ASTNode.types.PREVIOUS: { - const connection = this.location as Connection; - return ASTNode.createBlockNode(connection.getSourceBlock()); - } - case ASTNode.types.NEXT: { - const connection = this.location as Connection; - const targetConnection = connection.targetConnection; - return targetConnection - ? ASTNode.createConnectionNode(targetConnection) - : null; - } - case ASTNode.types.BUTTON: - return this.navigateFlyoutContents(true); - } - - return null; - } - - /** - * Find the element one level below and all the way to the left of the current - * location. - * - * @returns An AST node that wraps the next field, connection, workspace, or - * block. Or null if there is nothing below this node. - */ - in(): ASTNode | null { - switch (this.type) { - case ASTNode.types.WORKSPACE: { - const workspace = this.location as Workspace; - const topBlocks = workspace.getTopBlocks(true); - if (topBlocks.length > 0) { - return ASTNode.createStackNode(topBlocks[0]); - } - break; - } - case ASTNode.types.STACK: { - const block = this.location as Block; - return this.findTopASTNodeForBlock(block); - } - case ASTNode.types.BLOCK: { - const block = this.location as Block; - return this.findFirstFieldOrInput(block); - } - case ASTNode.types.INPUT: { - const connection = this.location as Connection; - const targetConnection = connection.targetConnection; - return targetConnection - ? ASTNode.createConnectionNode(targetConnection) - : null; - } - } - - return null; - } - - /** - * Find the element to the left of the current element in the AST. - * - * @returns An AST node that wraps the previous field, connection, workspace - * or block. Or null if no node exists to the left. null. - */ - prev(): ASTNode | null { - switch (this.type) { - case ASTNode.types.STACK: - return this.navigateBetweenStacks(false); - - case ASTNode.types.OUTPUT: - return null; - - case ASTNode.types.FIELD: - return this.findPrevForField(); - - case ASTNode.types.INPUT: - return this.findPrevForInput(); - - case ASTNode.types.BLOCK: { - const block = this.location as Block; - const topConnection = getParentConnection(block); - if (!topConnection) return null; - return ASTNode.createConnectionNode(topConnection); - } - case ASTNode.types.PREVIOUS: { - const connection = this.location as Connection; - const targetConnection = connection.targetConnection; - if (targetConnection && !targetConnection.getParentInput()) { - return ASTNode.createConnectionNode(targetConnection); - } - break; - } - case ASTNode.types.NEXT: { - const connection = this.location as Connection; - return ASTNode.createBlockNode(connection.getSourceBlock()); - } - case ASTNode.types.BUTTON: - return this.navigateFlyoutContents(false); - } - - return null; - } - - /** - * Find the next element that is one position above and all the way to the - * left of the current location. - * - * @returns An AST node that wraps the next field, connection, workspace or - * block. Or null if we are at the workspace level. - */ - out(): ASTNode | null { - switch (this.type) { - case ASTNode.types.STACK: { - const block = this.location as Block; - const blockPos = block.getRelativeToSurfaceXY(); - // TODO: Make sure this is in the bounds of the workspace. - const wsCoordinate = new Coordinate( - blockPos.x, - blockPos.y + ASTNode.DEFAULT_OFFSET_Y, - ); - return ASTNode.createWorkspaceNode(block.workspace, wsCoordinate); - } - case ASTNode.types.OUTPUT: { - const connection = this.location as Connection; - const target = connection.targetConnection; - if (target) { - return ASTNode.createConnectionNode(target); - } - return ASTNode.createStackNode(connection.getSourceBlock()); - } - case ASTNode.types.FIELD: { - const field = this.location as Field; - const block = field.getSourceBlock(); - if (!block) { - throw new Error( - 'The current AST location is not associated with a block', - ); - } - return ASTNode.createBlockNode(block); - } - case ASTNode.types.INPUT: { - const connection = this.location as Connection; - return ASTNode.createBlockNode(connection.getSourceBlock()); - } - case ASTNode.types.BLOCK: { - const block = this.location as Block; - return this.getOutAstNodeForBlock(block); - } - case ASTNode.types.PREVIOUS: { - const connection = this.location as Connection; - return this.getOutAstNodeForBlock(connection.getSourceBlock()); - } - case ASTNode.types.NEXT: { - const connection = this.location as Connection; - return this.getOutAstNodeForBlock(connection.getSourceBlock()); - } - } - - return null; - } - /** * Whether an AST node of the given type points to a connection. * diff --git a/core/keyboard_nav/block_navigation_policy.ts b/core/keyboard_nav/block_navigation_policy.ts new file mode 100644 index 00000000000..b073253f98e --- /dev/null +++ b/core/keyboard_nav/block_navigation_policy.ts @@ -0,0 +1,117 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {BlockSvg} from '../block_svg.js'; +import type {INavigable} from '../interfaces/i_navigable.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; +import type {RenderedConnection} from '../rendered_connection.js'; + +/** + * Set of rules controlling keyboard navigation from a block. + */ +export class BlockNavigationPolicy implements INavigationPolicy { + /** + * Returns the first child of the given block. + * + * @param current The block to return the first child of. + * @returns The first field or input of the given block, if any. + */ + getFirstChild(current: BlockSvg): INavigable | null { + for (const input of current.inputList) { + for (const field of input.fieldRow) { + return field; + } + if (input.connection) return input.connection as RenderedConnection; + } + + return null; + } + + /** + * Returns the parent of the given block. + * + * @param current The block to return the parent of. + * @returns The top block of the given block's stack, or the connection to + * which it is attached. + */ + getParent(current: BlockSvg): INavigable | null { + const topBlock = current.getTopStackBlock(); + + return ( + (this.getParentConnection(topBlock)?.targetConnection?.getParentInput() + ?.connection as RenderedConnection) ?? topBlock + ); + } + + /** + * Returns the next peer node of the given block. + * + * @param current The block to find the following element of. + * @returns The first block of the next stack if the given block is a terminal + * block, or its next connection. + */ + getNextSibling(current: BlockSvg): INavigable | null { + const nextConnection = current.nextConnection; + if (!current.outputConnection?.targetConnection && !nextConnection) { + // If this block has no connected output connection and no next + // connection, it must be the last block in the stack, so its next sibling + // is the first block of the next stack on the workspace. + const topBlocks = current.workspace.getTopBlocks(true); + let targetIndex = topBlocks.indexOf(current.getRootBlock()) + 1; + if (targetIndex >= topBlocks.length) { + targetIndex = 0; + } + const previousBlock = topBlocks[targetIndex]; + return this.getParentConnection(previousBlock) ?? previousBlock; + } + + return nextConnection; + } + + /** + * Returns the previous peer node of the given block. + * + * @param current The block to find the preceding element of. + * @returns The block's previous/output connection, or the last + * connection/block of the previous block stack if it is a root block. + */ + getPreviousSibling(current: BlockSvg): INavigable | null { + const parentConnection = this.getParentConnection(current); + if (parentConnection) return parentConnection; + + // If this block has no output/previous connection, it must be a root block, + // so its previous sibling is the last connection of the last block of the + // previous stack on the workspace. + const topBlocks = current.workspace.getTopBlocks(true); + let targetIndex = topBlocks.indexOf(current.getRootBlock()) - 1; + if (targetIndex < 0) { + targetIndex = topBlocks.length - 1; + } + + const lastBlock = topBlocks[targetIndex] + .getDescendants(true) + .reverse() + .pop(); + + return lastBlock?.nextConnection ?? lastBlock ?? null; + } + + /** + * Gets the parent connection on a block. + * This is either an output connection, previous connection or undefined. + * If both connections exist return the one that is actually connected + * to another block. + * + * @param block The block to find the parent connection on. + * @returns The connection connecting to the parent of the block. + */ + protected getParentConnection(block: BlockSvg) { + if (!block.outputConnection || block.previousConnection?.isConnected()) { + return block.previousConnection; + } + return block.outputConnection; + } +} diff --git a/core/keyboard_nav/connection_navigation_policy.ts b/core/keyboard_nav/connection_navigation_policy.ts new file mode 100644 index 00000000000..1dfb0f64edd --- /dev/null +++ b/core/keyboard_nav/connection_navigation_policy.ts @@ -0,0 +1,169 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {BlockSvg} from '../block_svg.js'; +import {ConnectionType} from '../connection_type.js'; +import type {INavigable} from '../interfaces/i_navigable.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; +import type {RenderedConnection} from '../rendered_connection.js'; + +/** + * Set of rules controlling keyboard navigation from a connection. + */ +export class ConnectionNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns the first child of the given connection. + * + * @param current The connection to return the first child of. + * @returns The connection's first child element, or null if not none. + */ + getFirstChild(current: RenderedConnection): INavigable | null { + if (current.getParentInput()) { + return current.targetConnection; + } + + return null; + } + + /** + * Returns the parent of the given connection. + * + * @param current The connection to return the parent of. + * @returns The given connection's parent connection or block. + */ + getParent(current: RenderedConnection): INavigable | null { + if (current.type === ConnectionType.OUTPUT_VALUE) { + return current.targetConnection ?? current.getSourceBlock(); + } else if (current.getParentInput()) { + return current.getSourceBlock(); + } + + const topBlock = current.getSourceBlock().getTopStackBlock(); + return ( + (this.getParentConnection(topBlock)?.targetConnection?.getParentInput() + ?.connection as RenderedConnection) ?? topBlock + ); + } + + /** + * Returns the next element following the given connection. + * + * @param current The connection to navigate from. + * @returns The field, input connection or block following this connection. + */ + getNextSibling(current: RenderedConnection): INavigable | null { + if (current.getParentInput()) { + const parentInput = current.getParentInput(); + const block = parentInput?.getSourceBlock(); + if (!block || !parentInput) return null; + + const curIdx = block.inputList.indexOf(parentInput); + for (let i = curIdx + 1; i < block.inputList.length; i++) { + const input = block.inputList[i]; + const fieldRow = input.fieldRow; + if (fieldRow.length) return fieldRow[0]; + if (input.connection) return input.connection as RenderedConnection; + } + + return null; + } else if (current.type === ConnectionType.NEXT_STATEMENT) { + const nextBlock = current.targetConnection; + // If this connection is the last one in the stack, our next sibling is + // the next block stack. + const sourceBlock = current.getSourceBlock(); + if ( + !nextBlock && + sourceBlock.getRootBlock().lastConnectionInStack(false) === current + ) { + const topBlocks = sourceBlock.workspace.getTopBlocks(true); + let targetIndex = topBlocks.indexOf(sourceBlock.getRootBlock()) + 1; + if (targetIndex >= topBlocks.length) { + targetIndex = 0; + } + const nextBlock = topBlocks[targetIndex]; + return this.getParentConnection(nextBlock) ?? nextBlock; + } + + return nextBlock; + } + + return current.getSourceBlock(); + } + + /** + * Returns the element preceding the given connection. + * + * @param current The connection to navigate from. + * @returns The field, input connection or block preceding this connection. + */ + getPreviousSibling(current: RenderedConnection): INavigable | null { + if (current.getParentInput()) { + const parentInput = current.getParentInput(); + const block = parentInput?.getSourceBlock(); + if (!block || !parentInput) return null; + + const curIdx = block.inputList.indexOf(parentInput); + for (let i = curIdx; i >= 0; i--) { + const input = block.inputList[i]; + if (input.connection && input !== parentInput) { + return input.connection as RenderedConnection; + } + const fieldRow = input.fieldRow; + if (fieldRow.length) return fieldRow[fieldRow.length - 1]; + } + return null; + } else if ( + current.type === ConnectionType.PREVIOUS_STATEMENT || + current.type === ConnectionType.OUTPUT_VALUE + ) { + const previousConnection = + current.targetConnection && !current.targetConnection.getParentInput() + ? current.targetConnection + : null; + + // If this connection is a disconnected previous/output connection, our + // previous sibling is the previous block stack's last connection/block. + const sourceBlock = current.getSourceBlock(); + if ( + !previousConnection && + this.getParentConnection(sourceBlock.getRootBlock()) === current + ) { + const topBlocks = sourceBlock.workspace.getTopBlocks(true); + let targetIndex = topBlocks.indexOf(sourceBlock.getRootBlock()) - 1; + if (targetIndex < 0) { + targetIndex = topBlocks.length - 1; + } + const previousRootBlock = topBlocks[targetIndex]; + return ( + previousRootBlock.lastConnectionInStack(false) ?? previousRootBlock + ); + } + + return previousConnection; + } else if (current.type === ConnectionType.NEXT_STATEMENT) { + return current.getSourceBlock(); + } + return null; + } + + /** + * Gets the parent connection on a block. + * This is either an output connection, previous connection or undefined. + * If both connections exist return the one that is actually connected + * to another block. + * + * @param block The block to find the parent connection on. + * @returns The connection connecting to the parent of the block. + */ + protected getParentConnection(block: BlockSvg) { + if (!block.outputConnection || block.previousConnection?.isConnected()) { + return block.previousConnection; + } + return block.outputConnection; + } +} diff --git a/core/keyboard_nav/field_navigation_policy.ts b/core/keyboard_nav/field_navigation_policy.ts new file mode 100644 index 00000000000..a0e43001e4f --- /dev/null +++ b/core/keyboard_nav/field_navigation_policy.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {BlockSvg} from '../block_svg.js'; +import type {Field} from '../field.js'; +import type {INavigable} from '../interfaces/i_navigable.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; +import type {RenderedConnection} from '../rendered_connection.js'; + +/** + * Set of rules controlling keyboard navigation from a field. + */ +export class FieldNavigationPolicy implements INavigationPolicy> { + /** + * Returns null since fields do not have children. + * + * @param _current The field to navigate from. + * @returns Null. + */ + getFirstChild(_current: Field): INavigable | null { + return null; + } + + /** + * Returns the parent block of the given field. + * + * @param current The field to navigate from. + * @returns The given field's parent block. + */ + getParent(current: Field): INavigable | null { + return current.getSourceBlock() as BlockSvg; + } + + /** + * Returns the next field or input following the given field. + * + * @param current The field to navigate from. + * @returns The next field or input in the given field's block. + */ + getNextSibling(current: Field): INavigable | null { + const input = current.getParentInput(); + const block = current.getSourceBlock(); + if (!block) return null; + + const curIdx = block.inputList.indexOf(input); + let fieldIdx = input.fieldRow.indexOf(current) + 1; + for (let i = curIdx; i < block.inputList.length; i++) { + const newInput = block.inputList[i]; + const fieldRow = newInput.fieldRow; + if (fieldIdx < fieldRow.length) return fieldRow[fieldIdx]; + fieldIdx = 0; + if (newInput.connection) { + return newInput.connection as RenderedConnection; + } + } + return null; + } + + /** + * Returns the field or input preceding the given field. + * + * @param current The field to navigate from. + * @returns The preceding field or input in the given field's block. + */ + getPreviousSibling(current: Field): INavigable | null { + const parentInput = current.getParentInput(); + const block = current.getSourceBlock(); + if (!block) return null; + + const curIdx = block.inputList.indexOf(parentInput); + let fieldIdx = parentInput.fieldRow.indexOf(current) - 1; + for (let i = curIdx; i >= 0; i--) { + const input = block.inputList[i]; + if (input.connection && input !== parentInput) { + return input.connection as RenderedConnection; + } + const fieldRow = input.fieldRow; + if (fieldIdx > -1) return fieldRow[fieldIdx]; + + // Reset the fieldIdx to the length of the field row of the previous + // input. + if (i - 1 >= 0) { + fieldIdx = block.inputList[i - 1].fieldRow.length - 1; + } + } + return null; + } +} diff --git a/core/keyboard_nav/flyout_button_navigation_policy.ts b/core/keyboard_nav/flyout_button_navigation_policy.ts new file mode 100644 index 00000000000..8819bf588c6 --- /dev/null +++ b/core/keyboard_nav/flyout_button_navigation_policy.ts @@ -0,0 +1,56 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {FlyoutButton} from '../flyout_button.js'; +import type {INavigable} from '../interfaces/i_navigable.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from a flyout button. + */ +export class FlyoutButtonNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns null since flyout buttons have no children. + * + * @param _current The FlyoutButton instance to navigate from. + * @returns Null. + */ + getFirstChild(_current: FlyoutButton): INavigable | null { + return null; + } + + /** + * Returns the parent workspace of the given flyout button. + * + * @param current The FlyoutButton instance to navigate from. + * @returns The given flyout button's parent workspace. + */ + getParent(current: FlyoutButton): INavigable | null { + return current.getWorkspace(); + } + + /** + * Returns null since inter-item navigation is done by FlyoutNavigationPolicy. + * + * @param _current The FlyoutButton instance to navigate from. + * @returns Null. + */ + getNextSibling(_current: FlyoutButton): INavigable | null { + return null; + } + + /** + * Returns null since inter-item navigation is done by FlyoutNavigationPolicy. + * + * @param _current The FlyoutButton instance to navigate from. + * @returns Null. + */ + getPreviousSibling(_current: FlyoutButton): INavigable | null { + return null; + } +} diff --git a/core/keyboard_nav/flyout_navigation_policy.ts b/core/keyboard_nav/flyout_navigation_policy.ts new file mode 100644 index 00000000000..6a89a6fbdc6 --- /dev/null +++ b/core/keyboard_nav/flyout_navigation_policy.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFlyout} from '../interfaces/i_flyout.js'; +import type {INavigable} from '../interfaces/i_navigable.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Generic navigation policy that navigates between items in the flyout. + */ +export class FlyoutNavigationPolicy implements INavigationPolicy { + /** + * Creates a new FlyoutNavigationPolicy instance. + * + * @param policy The policy to defer to for parents/children. + * @param flyout The flyout this policy will control navigation in. + */ + constructor( + private policy: INavigationPolicy, + private flyout: IFlyout, + ) {} + + /** + * Returns null to prevent navigating into flyout items. + * + * @param _current The flyout item to navigate from. + * @returns Null to prevent navigating into flyout items. + */ + getFirstChild(_current: T): INavigable | null { + return null; + } + + /** + * Returns the parent of the given flyout item. + * + * @param current The flyout item to navigate from. + * @returns The parent of the given flyout item. + */ + getParent(current: T): INavigable | null { + return this.policy.getParent(current); + } + + /** + * Returns the next item in the flyout relative to the given item. + * + * @param current The flyout item to navigate from. + * @returns The flyout item following the given one. + */ + getNextSibling(current: T): INavigable | null { + const flyoutContents = this.flyout.getContents(); + if (!flyoutContents) return null; + + let index = flyoutContents.findIndex( + (flyoutItem) => flyoutItem.getElement() === current, + ); + + if (index === -1) return null; + index++; + if (index >= flyoutContents.length) { + index = 0; + } + + return flyoutContents[index].getElement(); + } + + /** + * Returns the previous item in the flyout relative to the given item. + * + * @param current The flyout item to navigate from. + * @returns The flyout item preceding the given one. + */ + getPreviousSibling(current: T): INavigable | null { + const flyoutContents = this.flyout.getContents(); + if (!flyoutContents) return null; + + let index = flyoutContents.findIndex( + (flyoutItem) => flyoutItem.getElement() === current, + ); + + if (index === -1) return null; + index--; + if (index < 0) { + index = flyoutContents.length - 1; + } + + return flyoutContents[index].getElement(); + } +} diff --git a/core/keyboard_nav/flyout_separator_navigation_policy.ts b/core/keyboard_nav/flyout_separator_navigation_policy.ts new file mode 100644 index 00000000000..d104c4c0074 --- /dev/null +++ b/core/keyboard_nav/flyout_separator_navigation_policy.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {FlyoutSeparator} from '../flyout_separator.js'; +import type {INavigable} from '../interfaces/i_navigable.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from a flyout separator. + * This is a no-op placeholder, since flyout separators can't be navigated to. + */ +export class FlyoutSeparatorNavigationPolicy + implements INavigationPolicy +{ + getFirstChild(_current: FlyoutSeparator): INavigable | null { + return null; + } + + getParent(_current: FlyoutSeparator): INavigable | null { + return null; + } + + getNextSibling(_current: FlyoutSeparator): INavigable | null { + return null; + } + + getPreviousSibling(_current: FlyoutSeparator): INavigable | null { + return null; + } +} diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 196a0854763..67adade56fc 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -13,22 +13,37 @@ * @author aschmiedt@google.com (Abby Schmiedt) */ -import type {Block} from '../block.js'; import {BlockSvg} from '../block_svg.js'; import * as common from '../common.js'; -import type {Connection} from '../connection.js'; import {ConnectionType} from '../connection_type.js'; -import type {Field} from '../field.js'; +import {Field} from '../field.js'; +import {FieldCheckbox} from '../field_checkbox.js'; +import {FieldDropdown} from '../field_dropdown.js'; +import {FieldImage} from '../field_image.js'; +import {FieldLabel} from '../field_label.js'; +import {FieldNumber} from '../field_number.js'; +import {FieldTextInput} from '../field_textinput.js'; +import {FlyoutButton} from '../flyout_button.js'; +import {FlyoutSeparator} from '../flyout_separator.js'; import {getFocusManager} from '../focus_manager.js'; import {isFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigable} from '../interfaces/i_navigable.js'; import * as registry from '../registry.js'; +import {RenderedConnection} from '../rendered_connection.js'; import type {MarkerSvg} from '../renderers/common/marker_svg.js'; import type {PathObject} from '../renderers/zelos/path_object.js'; import {Renderer} from '../renderers/zelos/renderer.js'; import * as dom from '../utils/dom.js'; -import type {WorkspaceSvg} from '../workspace_svg.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; import {ASTNode} from './ast_node.js'; +import {BlockNavigationPolicy} from './block_navigation_policy.js'; +import {ConnectionNavigationPolicy} from './connection_navigation_policy.js'; +import {FieldNavigationPolicy} from './field_navigation_policy.js'; +import {FlyoutButtonNavigationPolicy} from './flyout_button_navigation_policy.js'; +import {FlyoutNavigationPolicy} from './flyout_navigation_policy.js'; +import {FlyoutSeparatorNavigationPolicy} from './flyout_separator_navigation_policy.js'; import {Marker} from './marker.js'; +import {WorkspaceNavigationPolicy} from './workspace_navigation_policy.js'; /** Options object for LineCursor instances. */ export interface CursorOptions { @@ -54,7 +69,7 @@ export class LineCursor extends Marker { private readonly options: CursorOptions; /** Locations to try moving the cursor to after a deletion. */ - private potentialNodes: ASTNode[] | null = null; + private potentialNodes: INavigable[] | null = null; /** Whether the renderer is zelos-style. */ private isZelos = false; @@ -64,7 +79,7 @@ export class LineCursor extends Marker { * @param options Cursor options. */ constructor( - private readonly workspace: WorkspaceSvg, + protected readonly workspace: WorkspaceSvg, options?: Partial, ) { super(); @@ -72,6 +87,54 @@ export class LineCursor extends Marker { this.options = {...defaultOptions, ...options}; this.isZelos = workspace.getRenderer() instanceof Renderer; + + this.registerNavigationPolicies(); + } + + /** + * Registers default navigation policies for Blockly's built-in types with + * this cursor's workspace. + */ + protected registerNavigationPolicies() { + const navigator = this.workspace.getNavigator(); + + const blockPolicy = new BlockNavigationPolicy(); + if (this.workspace.isFlyout) { + const flyout = this.workspace.targetWorkspace?.getFlyout(); + if (flyout) { + navigator.set( + BlockSvg, + new FlyoutNavigationPolicy(blockPolicy, flyout), + ); + + const buttonPolicy = new FlyoutButtonNavigationPolicy(); + navigator.set( + FlyoutButton, + new FlyoutNavigationPolicy(buttonPolicy, flyout), + ); + + navigator.set( + FlyoutSeparator, + new FlyoutNavigationPolicy( + new FlyoutSeparatorNavigationPolicy(), + flyout, + ), + ); + } + } else { + navigator.set(BlockSvg, blockPolicy); + } + + navigator.set(RenderedConnection, new ConnectionNavigationPolicy()); + navigator.set(WorkspaceSvg, new WorkspaceNavigationPolicy()); + + const fieldPolicy = new FieldNavigationPolicy(); + navigator.set(FieldCheckbox, fieldPolicy); + navigator.set(FieldDropdown, fieldPolicy); + navigator.set(FieldImage, fieldPolicy); + navigator.set(FieldLabel, fieldPolicy); + navigator.set(FieldNumber, fieldPolicy); + navigator.set(FieldTextInput, fieldPolicy); } /** @@ -81,14 +144,14 @@ export class LineCursor extends Marker { * @returns The next node, or null if the current node is * not set or there is no next value. */ - next(): ASTNode | null { + next(): INavigable | null { const curNode = this.getCurNode(); if (!curNode) { return null; } const newNode = this.getNextNode( curNode, - this.validLineNode.bind(this), + this.workspace.isFlyout ? () => true : this.validLineNode.bind(this), true, ); @@ -105,14 +168,14 @@ export class LineCursor extends Marker { * @returns The next node, or null if the current node is * not set or there is no next value. */ - in(): ASTNode | null { + in(): INavigable | null { const curNode = this.getCurNode(); if (!curNode) { return null; } const newNode = this.getNextNode( curNode, - this.validInLineNode.bind(this), + this.workspace.isFlyout ? () => true : this.validInLineNode.bind(this), true, ); @@ -128,14 +191,14 @@ export class LineCursor extends Marker { * @returns The previous node, or null if the current node * is not set or there is no previous value. */ - prev(): ASTNode | null { + prev(): INavigable | null { const curNode = this.getCurNode(); if (!curNode) { return null; } const newNode = this.getPreviousNode( curNode, - this.validLineNode.bind(this), + this.workspace.isFlyout ? () => true : this.validLineNode.bind(this), true, ); @@ -152,14 +215,14 @@ export class LineCursor extends Marker { * @returns The previous node, or null if the current node * is not set or there is no previous value. */ - out(): ASTNode | null { + out(): INavigable | null { const curNode = this.getCurNode(); if (!curNode) { return null; } const newNode = this.getPreviousNode( curNode, - this.validInLineNode.bind(this), + this.workspace.isFlyout ? () => true : this.validInLineNode.bind(this), true, ); @@ -209,39 +272,26 @@ export class LineCursor extends Marker { * @param node The AST node to check. * @returns True if the node should be visited, false otherwise. */ - protected validLineNode(node: ASTNode | null): boolean { + protected validLineNode(node: INavigable | null): boolean { if (!node) return false; - const location = node.getLocation(); - const type = node && node.getType(); - switch (type) { - case ASTNode.types.BLOCK: - return !(location as Block).outputConnection?.isConnected(); - case ASTNode.types.INPUT: { - const connection = location as Connection; - return ( - connection.type === ConnectionType.NEXT_STATEMENT && - (this.options.stackConnections || !connection.isConnected()) - ); + + if (node instanceof BlockSvg) { + return !node.outputConnection?.isConnected(); + } else if (node instanceof RenderedConnection) { + if (node.type === ConnectionType.NEXT_STATEMENT) { + return this.options.stackConnections || !node.isConnected(); + } else if (node.type === ConnectionType.PREVIOUS_STATEMENT) { + return this.options.stackConnections && !node.isConnected(); } - case ASTNode.types.NEXT: - return ( - this.options.stackConnections || - !(location as Connection).isConnected() - ); - case ASTNode.types.PREVIOUS: - return ( - this.options.stackConnections && - !(location as Connection).isConnected() - ); - default: - return false; } + + return false; } /** * Returns true iff the given node can be visited by the cursor when * using the left/right arrow keys. Specifically, if the node is - * any node for which valideLineNode would return true, plus: + * any node for which validLineNode would return true, plus: * * - Any block. * - Any field that is not a full block field. @@ -251,25 +301,21 @@ export class LineCursor extends Marker { * @param node The AST node to check whether it is valid. * @returns True if the node should be visited, false otherwise. */ - protected validInLineNode(node: ASTNode | null): boolean { + protected validInLineNode(node: INavigable | null): boolean { if (!node) return false; if (this.validLineNode(node)) return true; - const location = node.getLocation(); - const type = node && node.getType(); - switch (type) { - case ASTNode.types.BLOCK: - return true; - case ASTNode.types.INPUT: - return !(location as Connection).isConnected(); - case ASTNode.types.FIELD: { - const field = node.getLocation() as Field; - return !( - field.getSourceBlock()?.isSimpleReporter() && field.isFullBlockField() - ); - } - default: - return false; + if (node instanceof BlockSvg || node instanceof Field) { + return true; + } else if ( + node instanceof RenderedConnection && + node.getParentInput() && + (node.type === ConnectionType.INPUT_VALUE || + node.type === ConnectionType.NEXT_STATEMENT) + ) { + return !node.isConnected(); } + + return false; } /** @@ -280,10 +326,9 @@ export class LineCursor extends Marker { * @param node The AST node to check whether it is valid. * @returns True if the node should be visited, false otherwise. */ - protected validNode(node: ASTNode | null): boolean { + protected validNode(node: INavigable | null): boolean { return ( - !!node && - (this.validInLineNode(node) || node.getType() === ASTNode.types.WORKSPACE) + !!node && (this.validInLineNode(node) || node instanceof WorkspaceSvg) ); } @@ -295,20 +340,32 @@ export class LineCursor extends Marker { * @param node The current position in the AST. * @param isValid A function true/false depending on whether the given node * should be traversed. + * @param visitedNodes A set of previously visited nodes used to avoid cycles. * @returns The next node in the traversal. */ private getNextNodeImpl( - node: ASTNode | null, - isValid: (p1: ASTNode | null) => boolean, - ): ASTNode | null { - if (!node) return null; - let newNode = node.in() || node.next(); + node: INavigable | null, + isValid: (p1: INavigable | null) => boolean, + visitedNodes: Set> = new Set>(), + ): INavigable | null { + if (!node || visitedNodes.has(node)) return null; + let newNode = + this.workspace.getNavigator().getFirstChild(node) || + this.workspace.getNavigator().getNextSibling(node); if (isValid(newNode)) return newNode; - if (newNode) return this.getNextNodeImpl(newNode, isValid); + if (newNode) { + visitedNodes.add(node); + return this.getNextNodeImpl(newNode, isValid, visitedNodes); + } - newNode = this.findSiblingOrParentSibling(node.out()); + newNode = this.findSiblingOrParentSibling( + this.workspace.getNavigator().getParent(node), + ); if (isValid(newNode)) return newNode; - if (newNode) return this.getNextNodeImpl(newNode, isValid); + if (newNode) { + visitedNodes.add(node); + return this.getNextNodeImpl(newNode, isValid, visitedNodes); + } return null; } @@ -323,18 +380,13 @@ export class LineCursor extends Marker { * @returns The next node in the traversal. */ getNextNode( - node: ASTNode | null, - isValid: (p1: ASTNode | null) => boolean, + node: INavigable | null, + isValid: (p1: INavigable | null) => boolean, loop: boolean, - ): ASTNode | null { - if (!node) return null; + ): INavigable | null { + if (!node || (!loop && this.getLastNode() === node)) return null; - const potential = this.getNextNodeImpl(node, isValid); - if (potential || !loop) return potential; - // Loop back. - const firstNode = this.getFirstNode(); - if (isValid(firstNode)) return firstNode; - return this.getNextNodeImpl(firstNode, isValid); + return this.getNextNodeImpl(node, isValid); } /** @@ -345,24 +397,31 @@ export class LineCursor extends Marker { * @param node The current position in the AST. * @param isValid A function true/false depending on whether the given node * should be traversed. + * @param visitedNodes A set of previously visited nodes used to avoid cycles. * @returns The previous node in the traversal or null if no previous node * exists. */ private getPreviousNodeImpl( - node: ASTNode | null, - isValid: (p1: ASTNode | null) => boolean, - ): ASTNode | null { - if (!node) return null; - let newNode: ASTNode | null = node.prev(); + node: INavigable | null, + isValid: (p1: INavigable | null) => boolean, + visitedNodes: Set> = new Set>(), + ): INavigable | null { + if (!node || visitedNodes.has(node)) return null; + let newNode: INavigable | null = this.workspace + .getNavigator() + .getPreviousSibling(node); if (newNode) { newNode = this.getRightMostChild(newNode); } else { - newNode = node.out(); + newNode = this.workspace.getNavigator().getParent(node); } if (isValid(newNode)) return newNode; - if (newNode) return this.getPreviousNodeImpl(newNode, isValid); + if (newNode) { + visitedNodes.add(node); + return this.getPreviousNodeImpl(newNode, isValid, visitedNodes); + } return null; } @@ -378,18 +437,13 @@ export class LineCursor extends Marker { * exists. */ getPreviousNode( - node: ASTNode | null, - isValid: (p1: ASTNode | null) => boolean, + node: INavigable | null, + isValid: (p1: INavigable | null) => boolean, loop: boolean, - ): ASTNode | null { - if (!node) return null; + ): INavigable | null { + if (!node || (!loop && this.getFirstNode() === node)) return null; - const potential = this.getPreviousNodeImpl(node, isValid); - if (potential || !loop) return potential; - // Loop back. - const lastNode = this.getLastNode(); - if (isValid(lastNode)) return lastNode; - return this.getPreviousNodeImpl(lastNode, isValid); + return this.getPreviousNodeImpl(node, isValid); } /** @@ -399,11 +453,15 @@ export class LineCursor extends Marker { * @param node The current position in the AST. * @returns The next sibling node, the parent's next sibling, or null. */ - private findSiblingOrParentSibling(node: ASTNode | null): ASTNode | null { + private findSiblingOrParentSibling( + node: INavigable | null, + ): INavigable | null { if (!node) return null; - const nextNode = node.next(); + const nextNode = this.workspace.getNavigator().getNextSibling(node); if (nextNode) return nextNode; - return this.findSiblingOrParentSibling(node.out()); + return this.findSiblingOrParentSibling( + this.workspace.getNavigator().getParent(node), + ); } /** @@ -413,13 +471,13 @@ export class LineCursor extends Marker { * @returns The right most child of the given node, or the node if no child * exists. */ - private getRightMostChild(node: ASTNode): ASTNode | null { - let newNode = node.in(); + private getRightMostChild(node: INavigable): INavigable | null { + let newNode = this.workspace.getNavigator().getFirstChild(node); if (!newNode) return node; for ( - let nextNode: ASTNode | null = newNode; + let nextNode: INavigable | null = newNode; nextNode; - nextNode = newNode.next() + nextNode = this.workspace.getNavigator().getNextSibling(newNode) ) { newNode = nextNode; } @@ -448,40 +506,30 @@ export class LineCursor extends Marker { * * @param deletedBlock The block that is being deleted. */ - preDelete(deletedBlock: Block) { + preDelete(deletedBlock: BlockSvg) { const curNode = this.getCurNode(); - const nodes: ASTNode[] = curNode ? [curNode] : []; + const nodes: INavigable[] = curNode ? [curNode] : []; // The connection to which the deleted block is attached. const parentConnection = deletedBlock.previousConnection?.targetConnection ?? deletedBlock.outputConnection?.targetConnection; if (parentConnection) { - const parentNode = ASTNode.createConnectionNode(parentConnection); - if (parentNode) nodes.push(parentNode); + nodes.push(parentConnection); } // The block connected to the next connection of the deleted block. const nextBlock = deletedBlock.getNextBlock(); if (nextBlock) { - const nextNode = ASTNode.createBlockNode(nextBlock); - if (nextNode) nodes.push(nextNode); + nodes.push(nextBlock); } // The parent block of the deleted block. const parentBlock = deletedBlock.getParent(); if (parentBlock) { - const parentNode = ASTNode.createBlockNode(parentBlock); - if (parentNode) nodes.push(parentNode); + nodes.push(parentBlock); } // A location on the workspace beneath the deleted block. // Move to the workspace. - const curBlock = curNode?.getSourceBlock(); - if (curBlock) { - const workspaceNode = ASTNode.createWorkspaceNode( - this.workspace, - curBlock.getRelativeToSurfaceXY(), - ); - if (workspaceNode) nodes.push(workspaceNode); - } + nodes.push(this.workspace); this.potentialNodes = nodes; } @@ -494,7 +542,10 @@ export class LineCursor extends Marker { this.potentialNodes = null; if (!nodes) throw new Error('must call preDelete first'); for (const node of nodes) { - if (this.validNode(node) && !node.getSourceBlock()?.disposed) { + if ( + this.validNode(node) && + !this.toASTNode(node)?.getSourceBlock()?.disposed + ) { this.setCurNode(node); return; } @@ -513,7 +564,7 @@ export class LineCursor extends Marker { * * @returns The current field, connection, or block the cursor is on. */ - override getCurNode(): ASTNode | null { + override getCurNode(): INavigable | null { if (!this.updateCurNodeFromFocus()) { // Fall back to selection if focus fails to sync. This can happen for // non-focusable nodes or for cases when focus may not properly propagate @@ -565,19 +616,17 @@ export class LineCursor extends Marker { * * @param newNode The new location of the cursor. */ - override setCurNode(newNode: ASTNode | null) { + override setCurNode(newNode: INavigable | null) { super.setCurNode(newNode); - const newNodeLocation = newNode?.getLocation(); - if (isFocusableNode(newNodeLocation)) { - getFocusManager().focusNode(newNodeLocation); + if (isFocusableNode(newNode)) { + getFocusManager().focusNode(newNode); } // Try to scroll cursor into view. - if (newNode?.getType() === ASTNode.types.BLOCK) { - const block = newNode.getLocation() as BlockSvg; - block.workspace.scrollBoundsIntoView( - block.getBoundingRectangleWithoutChildren(), + if (newNode instanceof BlockSvg) { + newNode.workspace.scrollBoundsIntoView( + newNode.getBoundingRectangleWithoutChildren(), ); } } @@ -664,7 +713,7 @@ export class LineCursor extends Marker { */ private isValueInputConnection(node: ASTNode) { if (node?.getType() !== ASTNode.types.INPUT) return false; - const connection = node.getLocation() as Connection; + const connection = node.getLocation() as RenderedConnection; return connection.type === ConnectionType.INPUT_VALUE; } @@ -674,7 +723,7 @@ export class LineCursor extends Marker { * @param node The input node to hide. */ private hideAtInput(node: ASTNode) { - const inputConnection = node.getLocation() as Connection; + const inputConnection = node.getLocation() as RenderedConnection; const sourceBlock = inputConnection.getSourceBlock() as BlockSvg; const input = inputConnection.getParentInput(); if (input) { @@ -690,7 +739,7 @@ export class LineCursor extends Marker { * @param node The input node to show. */ private showAtInput(node: ASTNode) { - const inputConnection = node.getLocation() as Connection; + const inputConnection = node.getLocation() as RenderedConnection; const sourceBlock = inputConnection.getSourceBlock() as BlockSvg; const input = inputConnection.getParentInput(); if (input) { @@ -713,7 +762,7 @@ export class LineCursor extends Marker { const curNode = super.getCurNode(); const selected = common.getSelected(); - if (selected === null && curNode?.getType() === ASTNode.types.BLOCK) { + if (selected === null && curNode instanceof BlockSvg) { this.setCurNode(null); return; } @@ -725,17 +774,13 @@ export class LineCursor extends Marker { if (selected.isShadow()) { // OK if the current node is on the parent OR the shadow block. // The former happens for clicks, the latter for keyboard nav. - if ( - curNode && - (curNode.getLocation() === block || - curNode.getLocation() === block.getParent()) - ) { + if (curNode && (curNode === block || curNode === block.getParent())) { return; } block = block.getParent(); } if (block) { - this.setCurNode(ASTNode.createBlockNode(block)); + this.setCurNode(block); } } } @@ -752,13 +797,7 @@ export class LineCursor extends Marker { if (focused instanceof BlockSvg) { const block: BlockSvg | null = focused; if (block && block.workspace === this.workspace) { - if (block.getRootBlock() === block && this.workspace.isFlyout) { - // This block actually represents a stack. Note that this is needed - // because ASTNode special cases stack for cross-block navigation. - this.setCurNode(ASTNode.createStackNode(block)); - } else { - this.setCurNode(ASTNode.createBlockNode(block)); - } + this.setCurNode(block); return true; } } @@ -771,10 +810,8 @@ export class LineCursor extends Marker { * * @returns The first navigable node on the workspace, or null. */ - getFirstNode(): ASTNode | null { - const topBlocks = this.workspace.getTopBlocks(true); - if (!topBlocks.length) return null; - return ASTNode.createTopNode(topBlocks[0]); + getFirstNode(): INavigable | null { + return this.workspace.getNavigator().getFirstChild(this.workspace); } /** @@ -782,29 +819,9 @@ export class LineCursor extends Marker { * * @returns The last navigable node on the workspace, or null. */ - getLastNode(): ASTNode | null { - // Loop back to last block if it exists. - const topBlocks = this.workspace.getTopBlocks(true); - if (!topBlocks.length) return null; - - // Find the last stack. - const lastTopBlockNode = ASTNode.createStackNode( - topBlocks[topBlocks.length - 1], - ); - let prevNode = lastTopBlockNode; - let nextNode: ASTNode | null = lastTopBlockNode; - // Iterate until you fall off the end of the stack. - while (nextNode) { - prevNode = nextNode; - nextNode = this.getNextNode( - prevNode, - (node) => { - return !!node; - }, - false, - ); - } - return prevNode; + getLastNode(): INavigable | null { + const first = this.getFirstNode(); + return this.getPreviousNode(first, () => true, true); } } diff --git a/core/keyboard_nav/marker.ts b/core/keyboard_nav/marker.ts index aaa8e7355db..7a810dfe6c1 100644 --- a/core/keyboard_nav/marker.ts +++ b/core/keyboard_nav/marker.ts @@ -12,8 +12,15 @@ */ // Former goog.module ID: Blockly.Marker +import {BlockSvg} from '../block_svg.js'; +import {Field} from '../field.js'; +import {FlyoutButton} from '../flyout_button.js'; +import type {INavigable} from '../interfaces/i_navigable.js'; +import {RenderedConnection} from '../rendered_connection.js'; import type {MarkerSvg} from '../renderers/common/marker_svg.js'; -import type {ASTNode} from './ast_node.js'; +import {Coordinate} from '../utils/coordinate.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; +import {ASTNode} from './ast_node.js'; /** * Class for a marker. @@ -24,7 +31,7 @@ export class Marker { colour: string | null = null; /** The current location of the marker. */ - private curNode: ASTNode | null = null; + protected curNode: INavigable | null = null; /** * The object in charge of drawing the visual representation of the current @@ -58,7 +65,7 @@ export class Marker { * * @returns The current field, connection, or block the marker is on. */ - getCurNode(): ASTNode | null { + getCurNode(): INavigable | null { return this.curNode; } @@ -67,10 +74,10 @@ export class Marker { * * @param newNode The new location of the marker, or null to remove it. */ - setCurNode(newNode: ASTNode | null) { + setCurNode(newNode: INavigable | null) { const oldNode = this.curNode; this.curNode = newNode; - this.drawer?.draw(oldNode, this.curNode); + this.drawer?.draw(this.toASTNode(oldNode), this.toASTNode(this.curNode)); } /** @@ -79,7 +86,8 @@ export class Marker { * @internal */ draw() { - this.drawer?.draw(this.curNode, this.curNode); + const node = this.toASTNode(this.curNode); + this.drawer?.draw(node, node); } /** Hide the marker SVG. */ @@ -93,4 +101,46 @@ export class Marker { this.drawer = null; this.curNode = null; } + + /** + * Converts an INavigable to a legacy ASTNode. + * + * @param node The INavigable instance to convert. + * @returns An ASTNode representation of the given object if possible, + * otherwise null. + */ + toASTNode(node: INavigable | null): ASTNode | null { + if (node instanceof BlockSvg) { + return ASTNode.createBlockNode(node); + } else if (node instanceof Field) { + return ASTNode.createFieldNode(node); + } else if (node instanceof WorkspaceSvg) { + return ASTNode.createWorkspaceNode(node, new Coordinate(0, 0)); + } else if (node instanceof FlyoutButton) { + return ASTNode.createButtonNode(node); + } else if (node instanceof RenderedConnection) { + return ASTNode.createConnectionNode(node); + } + + return null; + } + + /** + * Returns the block that this marker's current node is a child of. + * + * @returns The parent block of the marker's current node if any, otherwise + * null. + */ + getSourceBlock(): BlockSvg | null { + const curNode = this.getCurNode(); + if (curNode instanceof BlockSvg) { + return curNode; + } else if (curNode instanceof Field) { + return curNode.getSourceBlock() as BlockSvg; + } else if (curNode instanceof RenderedConnection) { + return curNode.getSourceBlock(); + } + + return null; + } } diff --git a/core/keyboard_nav/workspace_navigation_policy.ts b/core/keyboard_nav/workspace_navigation_policy.ts new file mode 100644 index 00000000000..91f3221b558 --- /dev/null +++ b/core/keyboard_nav/workspace_navigation_policy.ts @@ -0,0 +1,66 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {INavigable} from '../interfaces/i_navigable.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; + +/** + * Set of rules controlling keyboard navigation from a workspace. + */ +export class WorkspaceNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns the first child of the given workspace. + * + * @param current The workspace to return the first child of. + * @returns The top block of the first block stack, if any. + */ + getFirstChild(current: WorkspaceSvg): INavigable | null { + const blocks = current.getTopBlocks(true); + if (!blocks.length) return null; + const block = blocks[0]; + let topConnection = block.outputConnection; + if ( + !topConnection || + (block.previousConnection && block.previousConnection.isConnected()) + ) { + topConnection = block.previousConnection; + } + return topConnection ?? block; + } + + /** + * Returns the parent of the given workspace. + * + * @param _current The workspace to return the parent of. + * @returns Null. + */ + getParent(_current: WorkspaceSvg): INavigable | null { + return null; + } + + /** + * Returns the next sibling of the given workspace. + * + * @param _current The workspace to return the next sibling of. + * @returns Null. + */ + getNextSibling(_current: WorkspaceSvg): INavigable | null { + return null; + } + + /** + * Returns the previous sibling of the given workspace. + * + * @param _current The workspace to return the previous sibling of. + * @returns Null. + */ + getPreviousSibling(_current: WorkspaceSvg): INavigable | null { + return null; + } +} diff --git a/core/label_flyout_inflater.ts b/core/label_flyout_inflater.ts index e4f3e3b54db..ffa69ae4806 100644 --- a/core/label_flyout_inflater.ts +++ b/core/label_flyout_inflater.ts @@ -32,7 +32,7 @@ export class LabelFlyoutInflater implements IFlyoutInflater { ); label.show(); - return new FlyoutItem(label, LABEL_TYPE, true); + return new FlyoutItem(label, LABEL_TYPE); } /** diff --git a/core/navigator.ts b/core/navigator.ts new file mode 100644 index 00000000000..33e98c47d79 --- /dev/null +++ b/core/navigator.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {INavigable} from './interfaces/i_navigable.js'; +import type {INavigationPolicy} from './interfaces/i_navigation_policy.js'; + +type RuleMap = Map T, INavigationPolicy>; + +/** + * Class responsible for determining where focus should move in response to + * keyboard navigation commands. + */ +export class Navigator { + /** + * Map from classes to a corresponding ruleset to handle navigation from + * instances of that class. + */ + private rules: RuleMap = new Map(); + + /** + * Associates a navigation ruleset with its corresponding class. + * + * @param key The class whose object instances should have their navigation + * controlled by the associated policy. + * @param policy A ruleset that determines where focus should move starting + * from an instance of the given class. + */ + set>( + key: new (...args: any) => T, + policy: INavigationPolicy, + ) { + this.rules.set(key, policy); + } + + /** + * Returns the navigation ruleset associated with the given object instance's + * class. + * + * @param key An object to retrieve a navigation ruleset for. + * @returns The navigation ruleset of objects of the given object's class, or + * undefined if no ruleset has been registered for the object's class. + */ + private get>( + key: T, + ): INavigationPolicy | undefined { + return this.rules.get(key.getClass()); + } + + /** + * Returns the first child of the given object instance, if any. + * + * @param current The object to retrieve the first child of. + * @returns The first child node of the given object, if any. + */ + getFirstChild>(current: T): INavigable | null { + const result = this.get(current)?.getFirstChild(current); + if (!result) return null; + // If the child isn't navigable, don't traverse into it; check its peers. + if (!result.isNavigable()) return this.getNextSibling(result); + return result; + } + + /** + * Returns the parent of the given object instance, if any. + * + * @param current The object to retrieve the parent of. + * @returns The parent node of the given object, if any. + */ + getParent>(current: T): INavigable | null { + const result = this.get(current)?.getParent(current); + if (!result) return null; + if (!result.isNavigable()) return this.getParent(result); + return result; + } + + /** + * Returns the next sibling of the given object instance, if any. + * + * @param current The object to retrieve the next sibling node of. + * @returns The next sibling node of the given object, if any. + */ + getNextSibling>(current: T): INavigable | null { + const result = this.get(current)?.getNextSibling(current); + if (!result) return null; + if (!result.isNavigable()) return this.getNextSibling(result); + return result; + } + + /** + * Returns the previous sibling of the given object instance, if any. + * + * @param current The object to retrieve the previous sibling node of. + * @returns The previous sibling node of the given object, if any. + */ + getPreviousSibling>( + current: T, + ): INavigable | null { + const result = this.get(current)?.getPreviousSibling(current); + if (!result) return null; + if (!result.isNavigable()) return this.getPreviousSibling(result); + return result; + } +} diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index 7ada1b9f6b7..858bc3d1d35 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -25,6 +25,7 @@ import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {hasBubble} from './interfaces/i_has_bubble.js'; +import type {INavigable} from './interfaces/i_navigable.js'; import * as internalConstants from './internal_constants.js'; import {Coordinate} from './utils/coordinate.js'; import * as svgMath from './utils/svg_math.js'; @@ -38,7 +39,7 @@ const BUMP_RANDOMNESS = 10; */ export class RenderedConnection extends Connection - implements IContextMenu, IFocusableNode + implements IContextMenu, IFocusableNode, INavigable { // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. sourceBlock_!: BlockSvg; @@ -677,6 +678,24 @@ export class RenderedConnection | unknown | null as SVGElement | null; } + + /** + * Returns whether or not this connection is keyboard-navigable. + * + * @returns True. + */ + isNavigable() { + return true; + } + + /** + * Returns this connection's class for keyboard navigation. + * + * @returns RenderedConnection. + */ + getClass() { + return RenderedConnection; + } } export namespace RenderedConnection { diff --git a/core/separator_flyout_inflater.ts b/core/separator_flyout_inflater.ts index 63e53355478..0c7897b0f03 100644 --- a/core/separator_flyout_inflater.ts +++ b/core/separator_flyout_inflater.ts @@ -43,7 +43,7 @@ export class SeparatorFlyoutInflater implements IFlyoutInflater { ? SeparatorAxis.X : SeparatorAxis.Y; const separator = new FlyoutSeparator(0, flyoutAxis); - return new FlyoutItem(separator, SEPARATOR_TYPE, false); + return new FlyoutItem(separator, SEPARATOR_TYPE); } /** diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index e67634e272b..a8930fbb44e 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -53,12 +53,14 @@ import { } from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; +import type {INavigable} from './interfaces/i_navigable.js'; import type {IToolbox} from './interfaces/i_toolbox.js'; import type {LineCursor} from './keyboard_nav/line_cursor.js'; import type {Marker} from './keyboard_nav/marker.js'; import {LayerManager} from './layer_manager.js'; import {MarkerManager} from './marker_manager.js'; import {Msg} from './msg.js'; +import {Navigator} from './navigator.js'; import {Options} from './options.js'; import * as Procedures from './procedures.js'; import * as registry from './registry.js'; @@ -99,7 +101,12 @@ const ZOOM_TO_FIT_MARGIN = 20; */ export class WorkspaceSvg extends Workspace - implements IASTNodeLocationSvg, IContextMenu, IFocusableNode, IFocusableTree + implements + IASTNodeLocationSvg, + IContextMenu, + IFocusableNode, + IFocusableTree, + INavigable { /** * A wrapper function called when a resize event occurs. @@ -336,6 +343,12 @@ export class WorkspaceSvg svgBubbleCanvas_!: SVGElement; zoomControls_: ZoomControls | null = null; + /** + * Navigator that handles moving focus between items in this workspace in + * response to keyboard navigation commands. + */ + private navigator = new Navigator(); + /** * @param options Dictionary of options. */ @@ -2790,6 +2803,34 @@ export class WorkspaceSvg if (flyout && isAutoHideable(flyout)) flyout.autoHide(false); } } + + /** + * Returns the class of this workspace. + * + * @returns WorkspaceSvg. + */ + getClass() { + return WorkspaceSvg; + } + + /** + * Returns whether or not this workspace is keyboard-navigable. + * + * @returns True. + */ + isNavigable() { + return true; + } + + /** + * Returns an object responsible for coordinating movement of focus between + * items on this workspace in response to keyboard navigation commands. + * + * @returns This workspace's Navigator instance. + */ + getNavigator(): Navigator { + return this.navigator; + } } /** diff --git a/tests/mocha/astnode_test.js b/tests/mocha/astnode_test.js deleted file mode 100644 index 7ffe8efb90a..00000000000 --- a/tests/mocha/astnode_test.js +++ /dev/null @@ -1,850 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import {ASTNode} from '../../build/src/core/keyboard_nav/ast_node.js'; -import {assert} from '../../node_modules/chai/chai.js'; -import { - sharedTestSetup, - sharedTestTeardown, - workspaceTeardown, -} from './test_helpers/setup_teardown.js'; - -suite('ASTNode', function () { - setup(function () { - sharedTestSetup.call(this); - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'input_statement', - 'message0': '%1 %2 %3 %4', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - { - 'type': 'input_value', - 'name': 'NAME', - }, - { - 'type': 'input_statement', - 'name': 'NAME', - }, - ], - 'previousStatement': null, - 'nextStatement': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'value_input', - 'message0': '%1', - 'args0': [ - { - 'type': 'input_value', - 'name': 'NAME', - }, - ], - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'field_input', - 'message0': '%1', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - ], - 'output': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - ]); - this.workspace = new Blockly.Workspace(); - this.cursor = this.workspace.cursor; - const statementInput1 = this.workspace.newBlock('input_statement'); - const statementInput2 = this.workspace.newBlock('input_statement'); - const statementInput3 = this.workspace.newBlock('input_statement'); - const statementInput4 = this.workspace.newBlock('input_statement'); - const fieldWithOutput = this.workspace.newBlock('field_input'); - const valueInput = this.workspace.newBlock('value_input'); - - statementInput1.nextConnection.connect(statementInput2.previousConnection); - statementInput1.inputList[0].connection.connect( - fieldWithOutput.outputConnection, - ); - statementInput2.inputList[1].connection.connect( - statementInput3.previousConnection, - ); - - this.blocks = { - statementInput1: statementInput1, - statementInput2: statementInput2, - statementInput3: statementInput3, - statementInput4: statementInput4, - fieldWithOutput: fieldWithOutput, - valueInput: valueInput, - }; - }); - teardown(function () { - sharedTestTeardown.call(this); - }); - - suite('HelperFunctions', function () { - test('findNextForInput', function () { - const input = this.blocks.statementInput1.inputList[0]; - const input2 = this.blocks.statementInput1.inputList[1]; - const connection = input.connection; - const node = ASTNode.createConnectionNode(connection); - const newASTNode = node.findNextForInput(input); - assert.equal(newASTNode.getLocation(), input2.connection); - }); - - test('findPrevForInput', function () { - const input = this.blocks.statementInput1.inputList[0]; - const input2 = this.blocks.statementInput1.inputList[1]; - const connection = input2.connection; - const node = ASTNode.createConnectionNode(connection); - const newASTNode = node.findPrevForInput(input2); - assert.equal(newASTNode.getLocation(), input.connection); - }); - - test('findNextForField', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - const field2 = this.blocks.statementInput1.inputList[0].fieldRow[1]; - const node = ASTNode.createFieldNode(field); - const newASTNode = node.findNextForField(field); - assert.equal(newASTNode.getLocation(), field2); - }); - - test('findPrevForField', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - const field2 = this.blocks.statementInput1.inputList[0].fieldRow[1]; - const node = ASTNode.createFieldNode(field2); - const newASTNode = node.findPrevForField(field2); - assert.equal(newASTNode.getLocation(), field); - }); - - test('navigateBetweenStacks_Forward', function () { - const node = new ASTNode( - ASTNode.types.NEXT, - this.blocks.statementInput1.nextConnection, - ); - const newASTNode = node.navigateBetweenStacks(true); - assert.equal(newASTNode.getLocation(), this.blocks.statementInput4); - }); - - test('navigateBetweenStacks_Backward', function () { - const node = new ASTNode( - ASTNode.types.BLOCK, - this.blocks.statementInput4, - ); - const newASTNode = node.navigateBetweenStacks(false); - assert.equal(newASTNode.getLocation(), this.blocks.statementInput1); - }); - test('getOutAstNodeForBlock', function () { - const node = new ASTNode( - ASTNode.types.BLOCK, - this.blocks.statementInput2, - ); - const newASTNode = node.getOutAstNodeForBlock( - this.blocks.statementInput2, - ); - assert.equal(newASTNode.getLocation(), this.blocks.statementInput1); - }); - test('getOutAstNodeForBlock_OneBlock', function () { - const node = new ASTNode( - ASTNode.types.BLOCK, - this.blocks.statementInput4, - ); - const newASTNode = node.getOutAstNodeForBlock( - this.blocks.statementInput4, - ); - assert.equal(newASTNode.getLocation(), this.blocks.statementInput4); - }); - test('findFirstFieldOrInput_', function () { - const node = new ASTNode( - ASTNode.types.BLOCK, - this.blocks.statementInput4, - ); - const field = this.blocks.statementInput4.inputList[0].fieldRow[0]; - const newASTNode = node.findFirstFieldOrInput( - this.blocks.statementInput4, - ); - assert.equal(newASTNode.getLocation(), field); - }); - }); - - suite('NavigationFunctions', function () { - setup(function () { - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'top_connection', - 'message0': '', - 'previousStatement': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'start_block', - 'message0': '', - 'nextStatement': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'fields_and_input', - 'message0': '%1 hi %2 %3 %4', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - { - 'type': 'input_dummy', - }, - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - { - 'type': 'input_value', - 'name': 'NAME', - }, - ], - 'previousStatement': null, - 'nextStatement': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'two_fields', - 'message0': '%1 hi', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - ], - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'fields_and_input2', - 'message0': '%1 %2 %3 hi %4 bye', - 'args0': [ - { - 'type': 'input_value', - 'name': 'NAME', - }, - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - { - 'type': 'input_value', - 'name': 'NAME', - }, - { - 'type': 'input_statement', - 'name': 'NAME', - }, - ], - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'dummy_input', - 'message0': 'Hello', - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'dummy_inputValue', - 'message0': 'Hello %1 %2', - 'args0': [ - { - 'type': 'input_dummy', - }, - { - 'type': 'input_value', - 'name': 'NAME', - }, - ], - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'output_next', - 'message0': '', - 'output': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - 'nextStatement': null, - }, - ]); - const noNextConnection = this.workspace.newBlock('top_connection'); - const fieldAndInputs = this.workspace.newBlock('fields_and_input'); - const twoFields = this.workspace.newBlock('two_fields'); - const fieldAndInputs2 = this.workspace.newBlock('fields_and_input2'); - const noPrevConnection = this.workspace.newBlock('start_block'); - this.blocks.noNextConnection = noNextConnection; - this.blocks.fieldAndInputs = fieldAndInputs; - this.blocks.twoFields = twoFields; - this.blocks.fieldAndInputs2 = fieldAndInputs2; - this.blocks.noPrevConnection = noPrevConnection; - - const dummyInput = this.workspace.newBlock('dummy_input'); - const dummyInputValue = this.workspace.newBlock('dummy_inputValue'); - const fieldWithOutput2 = this.workspace.newBlock('field_input'); - this.blocks.dummyInput = dummyInput; - this.blocks.dummyInputValue = dummyInputValue; - this.blocks.fieldWithOutput2 = fieldWithOutput2; - - const secondBlock = this.workspace.newBlock('input_statement'); - const outputNextBlock = this.workspace.newBlock('output_next'); - this.blocks.secondBlock = secondBlock; - this.blocks.outputNextBlock = outputNextBlock; - }); - suite('Next', function () { - setup(function () { - this.singleBlockWorkspace = new Blockly.Workspace(); - const singleBlock = this.singleBlockWorkspace.newBlock('two_fields'); - this.blocks.singleBlock = singleBlock; - }); - teardown(function () { - workspaceTeardown.call(this, this.singleBlockWorkspace); - }); - - test('fromPreviousToBlock', function () { - const prevConnection = this.blocks.statementInput1.previousConnection; - const node = ASTNode.createConnectionNode(prevConnection); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), this.blocks.statementInput1); - }); - test('fromBlockToNext', function () { - const nextConnection = this.blocks.statementInput1.nextConnection; - const node = ASTNode.createBlockNode(this.blocks.statementInput1); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), nextConnection); - }); - test('fromBlockToNull', function () { - const node = ASTNode.createBlockNode(this.blocks.noNextConnection); - const nextNode = node.next(); - assert.isNull(nextNode); - }); - test('fromNextToPrevious', function () { - const nextConnection = this.blocks.statementInput1.nextConnection; - const prevConnection = this.blocks.statementInput2.previousConnection; - const node = ASTNode.createConnectionNode(nextConnection); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), prevConnection); - }); - test('fromNextToNull', function () { - const nextConnection = this.blocks.statementInput2.nextConnection; - const node = ASTNode.createConnectionNode(nextConnection); - const nextNode = node.next(); - assert.isNull(nextNode); - }); - test('fromInputToInput', function () { - const input = this.blocks.statementInput1.inputList[0]; - const inputConnection = - this.blocks.statementInput1.inputList[1].connection; - const node = ASTNode.createInputNode(input); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), inputConnection); - }); - test('fromInputToStatementInput', function () { - const input = this.blocks.fieldAndInputs2.inputList[1]; - const inputConnection = - this.blocks.fieldAndInputs2.inputList[2].connection; - const node = ASTNode.createInputNode(input); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), inputConnection); - }); - test('fromInputToField', function () { - const input = this.blocks.fieldAndInputs2.inputList[0]; - const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0]; - const node = ASTNode.createInputNode(input); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), field); - }); - test('fromInputToNull', function () { - const input = this.blocks.fieldAndInputs2.inputList[2]; - const node = ASTNode.createInputNode(input); - const nextNode = node.next(); - assert.isNull(nextNode); - }); - test('fromOutputToBlock', function () { - const output = this.blocks.fieldWithOutput.outputConnection; - const node = ASTNode.createConnectionNode(output); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), this.blocks.fieldWithOutput); - }); - test('fromFieldToInput', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[1]; - const inputConnection = - this.blocks.statementInput1.inputList[0].connection; - const node = ASTNode.createFieldNode(field); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), inputConnection); - }); - test('fromFieldToField', function () { - const field = this.blocks.fieldAndInputs.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - const field2 = this.blocks.fieldAndInputs.inputList[1].fieldRow[0]; - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), field2); - }); - test('fromFieldToNull', function () { - const field = this.blocks.twoFields.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - const nextNode = node.next(); - assert.isNull(nextNode); - }); - test('fromStackToStack', function () { - const node = ASTNode.createStackNode(this.blocks.statementInput1); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), this.blocks.statementInput4); - assert.equal(nextNode.getType(), ASTNode.types.STACK); - }); - test('fromStackToNull', function () { - const node = ASTNode.createStackNode(this.blocks.singleBlock); - const nextNode = node.next(); - assert.isNull(nextNode); - }); - }); - - suite('Previous', function () { - test('fromPreviousToNull', function () { - const prevConnection = this.blocks.statementInput1.previousConnection; - const node = ASTNode.createConnectionNode(prevConnection); - const prevNode = node.prev(); - assert.isNull(prevNode); - }); - test('fromPreviousToNext', function () { - const prevConnection = this.blocks.statementInput2.previousConnection; - const node = ASTNode.createConnectionNode(prevConnection); - const prevNode = node.prev(); - const nextConnection = this.blocks.statementInput1.nextConnection; - assert.equal(prevNode.getLocation(), nextConnection); - }); - test('fromPreviousToInput', function () { - const prevConnection = this.blocks.statementInput3.previousConnection; - const node = ASTNode.createConnectionNode(prevConnection); - const prevNode = node.prev(); - assert.isNull(prevNode); - }); - test('fromBlockToPrevious', function () { - const node = ASTNode.createBlockNode(this.blocks.statementInput1); - const prevNode = node.prev(); - const prevConnection = this.blocks.statementInput1.previousConnection; - assert.equal(prevNode.getLocation(), prevConnection); - }); - test('fromBlockToNull', function () { - const node = ASTNode.createBlockNode(this.blocks.noPrevConnection); - const prevNode = node.prev(); - assert.isNull(prevNode); - }); - test('fromBlockToOutput', function () { - const node = ASTNode.createBlockNode(this.blocks.fieldWithOutput); - const prevNode = node.prev(); - const outputConnection = this.blocks.fieldWithOutput.outputConnection; - assert.equal(prevNode.getLocation(), outputConnection); - }); - test('fromNextToBlock', function () { - const nextConnection = this.blocks.statementInput1.nextConnection; - const node = ASTNode.createConnectionNode(nextConnection); - const prevNode = node.prev(); - assert.equal(prevNode.getLocation(), this.blocks.statementInput1); - }); - test('fromInputToField', function () { - const input = this.blocks.statementInput1.inputList[0]; - const node = ASTNode.createInputNode(input); - const prevNode = node.prev(); - assert.equal(prevNode.getLocation(), input.fieldRow[1]); - }); - test('fromInputToNull', function () { - const input = this.blocks.fieldAndInputs2.inputList[0]; - const node = ASTNode.createInputNode(input); - const prevNode = node.prev(); - assert.isNull(prevNode); - }); - test('fromInputToInput', function () { - const input = this.blocks.fieldAndInputs2.inputList[2]; - const inputConnection = - this.blocks.fieldAndInputs2.inputList[1].connection; - const node = ASTNode.createInputNode(input); - const prevNode = node.prev(); - assert.equal(prevNode.getLocation(), inputConnection); - }); - test('fromOutputToNull', function () { - const output = this.blocks.fieldWithOutput.outputConnection; - const node = ASTNode.createConnectionNode(output); - const prevNode = node.prev(); - assert.isNull(prevNode); - }); - test('fromFieldToNull', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - const prevNode = node.prev(); - assert.isNull(prevNode); - }); - test('fromFieldToInput', function () { - const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0]; - const inputConnection = - this.blocks.fieldAndInputs2.inputList[0].connection; - const node = ASTNode.createFieldNode(field); - const prevNode = node.prev(); - assert.equal(prevNode.getLocation(), inputConnection); - }); - test('fromFieldToField', function () { - const field = this.blocks.fieldAndInputs.inputList[1].fieldRow[0]; - const field2 = this.blocks.fieldAndInputs.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - const prevNode = node.prev(); - assert.equal(prevNode.getLocation(), field2); - }); - test('fromStackToStack', function () { - const node = ASTNode.createStackNode(this.blocks.statementInput4); - const prevNode = node.prev(); - assert.equal(prevNode.getLocation(), this.blocks.statementInput1); - assert.equal(prevNode.getType(), ASTNode.types.STACK); - }); - }); - - suite('In', function () { - setup(function () { - this.emptyWorkspace = new Blockly.Workspace(); - }); - teardown(function () { - workspaceTeardown.call(this, this.emptyWorkspace); - }); - - test('fromInputToOutput', function () { - const input = this.blocks.statementInput1.inputList[0]; - const node = ASTNode.createInputNode(input); - const inNode = node.in(); - const outputConnection = this.blocks.fieldWithOutput.outputConnection; - assert.equal(inNode.getLocation(), outputConnection); - }); - test('fromInputToNull', function () { - const input = this.blocks.statementInput2.inputList[0]; - const node = ASTNode.createInputNode(input); - const inNode = node.in(); - assert.isNull(inNode); - }); - test('fromInputToPrevious', function () { - const input = this.blocks.statementInput2.inputList[1]; - const previousConnection = - this.blocks.statementInput3.previousConnection; - const node = ASTNode.createInputNode(input); - const inNode = node.in(); - assert.equal(inNode.getLocation(), previousConnection); - }); - test('fromBlockToInput', function () { - const input = this.blocks.valueInput.inputList[0]; - const node = ASTNode.createBlockNode(this.blocks.valueInput); - const inNode = node.in(); - assert.equal(inNode.getLocation(), input.connection); - }); - test('fromBlockToField', function () { - const node = ASTNode.createBlockNode(this.blocks.statementInput1); - const inNode = node.in(); - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - assert.equal(inNode.getLocation(), field); - }); - test('fromBlockToPrevious', function () { - const prevConnection = this.blocks.statementInput4.previousConnection; - const node = ASTNode.createStackNode(this.blocks.statementInput4); - const inNode = node.in(); - assert.equal(inNode.getLocation(), prevConnection); - assert.equal(inNode.getType(), ASTNode.types.PREVIOUS); - }); - test('fromBlockToNull_DummyInput', function () { - const node = ASTNode.createBlockNode(this.blocks.dummyInput); - const inNode = node.in(); - assert.isNull(inNode); - }); - test('fromBlockToInput_DummyInputValue', function () { - const node = ASTNode.createBlockNode(this.blocks.dummyInputValue); - const inputConnection = - this.blocks.dummyInputValue.inputList[1].connection; - const inNode = node.in(); - assert.equal(inNode.getLocation(), inputConnection); - }); - test('fromOuputToNull', function () { - const output = this.blocks.fieldWithOutput.outputConnection; - const node = ASTNode.createConnectionNode(output); - const inNode = node.in(); - assert.isNull(inNode); - }); - test('fromFieldToNull', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - const inNode = node.in(); - assert.isNull(inNode); - }); - test('fromWorkspaceToStack', function () { - const coordinate = new Blockly.utils.Coordinate(100, 100); - const node = ASTNode.createWorkspaceNode(this.workspace, coordinate); - const inNode = node.in(); - assert.equal(inNode.getLocation(), this.workspace.getTopBlocks()[0]); - assert.equal(inNode.getType(), ASTNode.types.STACK); - }); - test('fromWorkspaceToNull', function () { - const coordinate = new Blockly.utils.Coordinate(100, 100); - const node = ASTNode.createWorkspaceNode( - this.emptyWorkspace, - coordinate, - ); - const inNode = node.in(); - assert.isNull(inNode); - }); - test('fromStackToPrevious', function () { - const node = ASTNode.createStackNode(this.blocks.statementInput1); - const previous = this.blocks.statementInput1.previousConnection; - const inNode = node.in(); - assert.equal(inNode.getLocation(), previous); - assert.equal(inNode.getType(), ASTNode.types.PREVIOUS); - }); - test('fromStackToOutput', function () { - const node = ASTNode.createStackNode(this.blocks.fieldWithOutput2); - const output = this.blocks.fieldWithOutput2.outputConnection; - const inNode = node.in(); - assert.equal(inNode.getLocation(), output); - assert.equal(inNode.getType(), ASTNode.types.OUTPUT); - }); - test('fromStackToBlock', function () { - const node = ASTNode.createStackNode(this.blocks.dummyInput); - const inNode = node.in(); - assert.equal(inNode.getLocation(), this.blocks.dummyInput); - assert.equal(inNode.getType(), ASTNode.types.BLOCK); - }); - }); - - suite('Out', function () { - setup(function () { - const secondBlock = this.blocks.secondBlock; - const outputNextBlock = this.blocks.outputNextBlock; - this.blocks.noPrevConnection.nextConnection.connect( - secondBlock.previousConnection, - ); - secondBlock.inputList[0].connection.connect( - outputNextBlock.outputConnection, - ); - }); - - test('fromInputToBlock', function () { - const input = this.blocks.statementInput1.inputList[0]; - const node = ASTNode.createInputNode(input); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.BLOCK); - assert.equal(outNode.getLocation(), this.blocks.statementInput1); - }); - test('fromOutputToInput', function () { - const output = this.blocks.fieldWithOutput.outputConnection; - const node = ASTNode.createConnectionNode(output); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.INPUT); - assert.equal( - outNode.getLocation(), - this.blocks.statementInput1.inputList[0].connection, - ); - }); - test('fromOutputToStack', function () { - const output = this.blocks.fieldWithOutput2.outputConnection; - const node = ASTNode.createConnectionNode(output); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.fieldWithOutput2); - }); - test('fromFieldToBlock', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.BLOCK); - assert.equal(outNode.getLocation(), this.blocks.statementInput1); - }); - test('fromStackToWorkspace', function () { - const stub = sinon - .stub(this.blocks.statementInput4, 'getRelativeToSurfaceXY') - .returns({x: 10, y: 10}); - const node = ASTNode.createStackNode(this.blocks.statementInput4); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.WORKSPACE); - assert.equal(outNode.wsCoordinate.x, 10); - assert.equal(outNode.wsCoordinate.y, -10); - stub.restore(); - }); - test('fromPreviousToInput', function () { - const previous = this.blocks.statementInput3.previousConnection; - const inputConnection = - this.blocks.statementInput2.inputList[1].connection; - const node = ASTNode.createConnectionNode(previous); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.INPUT); - assert.equal(outNode.getLocation(), inputConnection); - }); - test('fromPreviousToStack', function () { - const previous = this.blocks.statementInput2.previousConnection; - const node = ASTNode.createConnectionNode(previous); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.statementInput1); - }); - test('fromNextToInput', function () { - const next = this.blocks.statementInput3.nextConnection; - const inputConnection = - this.blocks.statementInput2.inputList[1].connection; - const node = ASTNode.createConnectionNode(next); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.INPUT); - assert.equal(outNode.getLocation(), inputConnection); - }); - test('fromNextToStack', function () { - const next = this.blocks.statementInput2.nextConnection; - const node = ASTNode.createConnectionNode(next); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.statementInput1); - }); - test('fromNextToStack_NoPreviousConnection', function () { - const next = this.blocks.secondBlock.nextConnection; - const node = ASTNode.createConnectionNode(next); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.noPrevConnection); - }); - /** - * This is where there is a block with both an output connection and a - * next connection attached to an input. - */ - test('fromNextToInput_OutputAndPreviousConnection', function () { - const next = this.blocks.outputNextBlock.nextConnection; - const node = ASTNode.createConnectionNode(next); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.INPUT); - assert.equal( - outNode.getLocation(), - this.blocks.secondBlock.inputList[0].connection, - ); - }); - test('fromBlockToStack', function () { - const node = ASTNode.createBlockNode(this.blocks.statementInput2); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.statementInput1); - }); - test('fromBlockToInput', function () { - const input = this.blocks.statementInput2.inputList[1].connection; - const node = ASTNode.createBlockNode(this.blocks.statementInput3); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.INPUT); - assert.equal(outNode.getLocation(), input); - }); - test('fromTopBlockToStack', function () { - const node = ASTNode.createBlockNode(this.blocks.statementInput1); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.statementInput1); - }); - test('fromBlockToStack_OutputConnection', function () { - const node = ASTNode.createBlockNode(this.blocks.fieldWithOutput2); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.fieldWithOutput2); - }); - test('fromBlockToInput_OutputConnection', function () { - const node = ASTNode.createBlockNode(this.blocks.outputNextBlock); - const inputConnection = this.blocks.secondBlock.inputList[0].connection; - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.INPUT); - assert.equal(outNode.getLocation(), inputConnection); - }); - }); - - suite('createFunctions', function () { - test('createFieldNode', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - assert.equal(node.getLocation(), field); - assert.equal(node.getType(), ASTNode.types.FIELD); - assert.isFalse(node.isConnection()); - }); - test('createConnectionNode', function () { - const prevConnection = this.blocks.statementInput4.previousConnection; - const node = ASTNode.createConnectionNode(prevConnection); - assert.equal(node.getLocation(), prevConnection); - assert.equal(node.getType(), ASTNode.types.PREVIOUS); - assert.isTrue(node.isConnection()); - }); - test('createInputNode', function () { - const input = this.blocks.statementInput1.inputList[0]; - const node = ASTNode.createInputNode(input); - assert.equal(node.getLocation(), input.connection); - assert.equal(node.getType(), ASTNode.types.INPUT); - assert.isTrue(node.isConnection()); - }); - test('createWorkspaceNode', function () { - const coordinate = new Blockly.utils.Coordinate(100, 100); - const node = ASTNode.createWorkspaceNode(this.workspace, coordinate); - assert.equal(node.getLocation(), this.workspace); - assert.equal(node.getType(), ASTNode.types.WORKSPACE); - assert.equal(node.getWsCoordinate(), coordinate); - assert.isFalse(node.isConnection()); - }); - test('createStatementConnectionNode', function () { - const nextConnection = - this.blocks.statementInput1.inputList[1].connection; - const inputConnection = - this.blocks.statementInput1.inputList[1].connection; - const node = ASTNode.createConnectionNode(nextConnection); - assert.equal(node.getLocation(), inputConnection); - assert.equal(node.getType(), ASTNode.types.INPUT); - assert.isTrue(node.isConnection()); - }); - test('createTopNode-previous', function () { - const block = this.blocks.statementInput1; - const topNode = ASTNode.createTopNode(block); - assert.equal(topNode.getLocation(), block.previousConnection); - }); - test('createTopNode-block', function () { - const block = this.blocks.noPrevConnection; - const topNode = ASTNode.createTopNode(block); - assert.equal(topNode.getLocation(), block); - }); - test('createTopNode-output', function () { - const block = this.blocks.outputNextBlock; - const topNode = ASTNode.createTopNode(block); - assert.equal(topNode.getLocation(), block.outputConnection); - }); - }); - }); -}); diff --git a/tests/mocha/cursor_test.js b/tests/mocha/cursor_test.js index 55595df7b05..ded07a5617f 100644 --- a/tests/mocha/cursor_test.js +++ b/tests/mocha/cursor_test.js @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {ASTNode} from '../../build/src/core/keyboard_nav/ast_node.js'; import {assert} from '../../node_modules/chai/chai.js'; import {createRenderedBlock} from './test_helpers/block_definitions.js'; import { @@ -87,70 +86,63 @@ suite('Cursor', function () { }); test('Next - From a Previous connection go to the next block', function () { - const prevNode = ASTNode.createConnectionNode( - this.blocks.A.previousConnection, - ); + const prevNode = this.blocks.A.previousConnection; this.cursor.setCurNode(prevNode); this.cursor.next(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.A); + assert.equal(curNode, this.blocks.A); }); test('Next - From a block go to its statement input', function () { - const prevNode = ASTNode.createBlockNode(this.blocks.B); + const prevNode = this.blocks.B; this.cursor.setCurNode(prevNode); this.cursor.next(); const curNode = this.cursor.getCurNode(); - assert.equal( - curNode.getLocation(), - this.blocks.B.getInput('NAME4').connection, - ); + assert.equal(curNode, this.blocks.B.getInput('NAME4').connection); }); test('In - From attached input connection', function () { const fieldBlock = this.blocks.E; - const inputConnectionNode = ASTNode.createConnectionNode( - this.blocks.A.inputList[0].connection, - ); + const inputConnectionNode = this.blocks.A.inputList[0].connection; this.cursor.setCurNode(inputConnectionNode); this.cursor.in(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), fieldBlock); + assert.equal(curNode, fieldBlock); }); test('Prev - From previous connection does not skip over next connection', function () { const prevConnection = this.blocks.B.previousConnection; - const prevConnectionNode = ASTNode.createConnectionNode(prevConnection); + const prevConnectionNode = prevConnection; this.cursor.setCurNode(prevConnectionNode); this.cursor.prev(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.A.nextConnection); + assert.equal(curNode, this.blocks.A.nextConnection); }); test('Prev - From first connection loop to last next connection', function () { const prevConnection = this.blocks.A.previousConnection; - const prevConnectionNode = ASTNode.createConnectionNode(prevConnection); + const prevConnectionNode = prevConnection; this.cursor.setCurNode(prevConnectionNode); this.cursor.prev(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.D.nextConnection); + assert.equal(curNode, this.blocks.D.nextConnection); }); test('Out - From field does not skip over block node', function () { const field = this.blocks.E.inputList[0].fieldRow[0]; - const fieldNode = ASTNode.createFieldNode(field); + const fieldNode = field; this.cursor.setCurNode(fieldNode); this.cursor.out(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.E); + assert.equal(curNode, this.blocks.E); }); test('Out - From first connection loop to last next connection', function () { const prevConnection = this.blocks.A.previousConnection; - const prevConnectionNode = ASTNode.createConnectionNode(prevConnection); + const prevConnectionNode = prevConnection; this.cursor.setCurNode(prevConnectionNode); this.cursor.out(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.D.nextConnection); + assert.equal(curNode, this.blocks.D.nextConnection); }); }); suite('Searching', function () { @@ -216,11 +208,11 @@ suite('Cursor', function () { }); test('getFirstNode', function () { const node = this.cursor.getFirstNode(); - assert.equal(node.getLocation(), this.blockA); + assert.equal(node, this.blockA); }); test('getLastNode', function () { const node = this.cursor.getLastNode(); - assert.equal(node.getLocation(), this.blockA); + assert.equal(node, this.blockA); }); }); @@ -233,11 +225,11 @@ suite('Cursor', function () { }); test('getFirstNode', function () { const node = this.cursor.getFirstNode(); - assert.equal(node.getLocation(), this.blockA.previousConnection); + assert.equal(node, this.blockA.previousConnection); }); test('getLastNode', function () { const node = this.cursor.getLastNode(); - assert.equal(node.getLocation(), this.blockA.nextConnection); + assert.equal(node, this.blockA.nextConnection); }); }); @@ -250,11 +242,11 @@ suite('Cursor', function () { }); test('getFirstNode', function () { const node = this.cursor.getFirstNode(); - assert.equal(node.getLocation(), this.blockA.outputConnection); + assert.equal(node, this.blockA.outputConnection); }); test('getLastNode', function () { const node = this.cursor.getLastNode(); - assert.equal(node.getLocation(), this.blockA.inputList[0].connection); + assert.equal(node, this.blockA.inputList[0].connection); }); }); suite('one c-hat block', function () { @@ -266,11 +258,11 @@ suite('Cursor', function () { }); test('getFirstNode', function () { const node = this.cursor.getFirstNode(); - assert.equal(node.getLocation(), this.blockA); + assert.equal(node, this.blockA); }); test('getLastNode', function () { const node = this.cursor.getLastNode(); - assert.equal(node.getLocation(), this.blockA.inputList[0].connection); + assert.equal(node, this.blockA.inputList[0].connection); }); }); @@ -303,12 +295,12 @@ suite('Cursor', function () { test('getFirstNode', function () { const node = this.cursor.getFirstNode(); const blockA = this.workspace.getBlockById('A'); - assert.equal(node.getLocation(), blockA.previousConnection); + assert.equal(node, blockA.previousConnection); }); test('getLastNode', function () { const node = this.cursor.getLastNode(); const blockB = this.workspace.getBlockById('B'); - assert.equal(node.getLocation(), blockB.nextConnection); + assert.equal(node, blockB.nextConnection); }); }); @@ -343,14 +335,15 @@ suite('Cursor', function () { test('getFirstNode', function () { const node = this.cursor.getFirstNode(); const blockA = this.workspace.getBlockById('A'); - assert.equal(node.getLocation(), blockA.outputConnection); + assert.equal(node, blockA.outputConnection); }); test('getLastNode', function () { const node = this.cursor.getLastNode(); const blockB = this.workspace.getBlockById('B'); - assert.equal(node.getLocation(), blockB.inputList[0].connection); + assert.equal(node, blockB.inputList[0].connection); }); }); + suite('two stacks', function () { setup(function () { const state = { @@ -391,14 +384,14 @@ suite('Cursor', function () { }); test('getFirstNode', function () { const node = this.cursor.getFirstNode(); - const location = node.getLocation(); + const location = node; const previousConnection = this.workspace.getBlockById('A').previousConnection; assert.equal(location, previousConnection); }); test('getLastNode', function () { const node = this.cursor.getLastNode(); - const location = node.getLocation(); + const location = node; const nextConnection = this.workspace.getBlockById('D').nextConnection; assert.equal(location, nextConnection); }); @@ -447,7 +440,7 @@ suite('Cursor', function () { this.neverValid = () => false; this.alwaysValid = () => true; this.isConnection = (node) => { - return node && node.isConnection(); + return node && node instanceof Blockly.RenderedConnection; }; }); teardown(function () { @@ -489,9 +482,7 @@ suite('Cursor', function () { this.workspace.clear(); }); test('Never valid - start at top', function () { - const startNode = ASTNode.createConnectionNode( - this.blockA.previousConnection, - ); + const startNode = this.blockA.previousConnection; const nextNode = this.cursor.getNextNode( startNode, this.neverValid, @@ -500,7 +491,7 @@ suite('Cursor', function () { assert.isNull(nextNode); }); test('Never valid - start in middle', function () { - const startNode = ASTNode.createBlockNode(this.blockB); + const startNode = this.blockB; const nextNode = this.cursor.getNextNode( startNode, this.neverValid, @@ -509,9 +500,7 @@ suite('Cursor', function () { assert.isNull(nextNode); }); test('Never valid - start at end', function () { - const startNode = ASTNode.createConnectionNode( - this.blockC.nextConnection, - ); + const startNode = this.blockC.nextConnection; const nextNode = this.cursor.getNextNode( startNode, this.neverValid, @@ -521,29 +510,25 @@ suite('Cursor', function () { }); test('Always valid - start at top', function () { - const startNode = ASTNode.createConnectionNode( - this.blockA.previousConnection, - ); + const startNode = this.blockA.previousConnection; const nextNode = this.cursor.getNextNode( startNode, this.alwaysValid, false, ); - assert.equal(nextNode.getLocation(), this.blockA); + assert.equal(nextNode, this.blockA); }); test('Always valid - start in middle', function () { - const startNode = ASTNode.createBlockNode(this.blockB); + const startNode = this.blockB; const nextNode = this.cursor.getNextNode( startNode, this.alwaysValid, false, ); - assert.equal(nextNode.getLocation(), this.blockB.getField('FIELD')); + assert.equal(nextNode, this.blockB.getField('FIELD')); }); test('Always valid - start at end', function () { - const startNode = ASTNode.createConnectionNode( - this.blockC.nextConnection, - ); + const startNode = this.blockC.nextConnection; const nextNode = this.cursor.getNextNode( startNode, this.alwaysValid, @@ -553,29 +538,25 @@ suite('Cursor', function () { }); test('Valid if connection - start at top', function () { - const startNode = ASTNode.createConnectionNode( - this.blockA.previousConnection, - ); + const startNode = this.blockA.previousConnection; const nextNode = this.cursor.getNextNode( startNode, this.isConnection, false, ); - assert.equal(nextNode.getLocation(), this.blockA.nextConnection); + assert.equal(nextNode, this.blockA.nextConnection); }); test('Valid if connection - start in middle', function () { - const startNode = ASTNode.createBlockNode(this.blockB); + const startNode = this.blockB; const nextNode = this.cursor.getNextNode( startNode, this.isConnection, false, ); - assert.equal(nextNode.getLocation(), this.blockB.nextConnection); + assert.equal(nextNode, this.blockB.nextConnection); }); test('Valid if connection - start at end', function () { - const startNode = ASTNode.createConnectionNode( - this.blockC.nextConnection, - ); + const startNode = this.blockC.nextConnection; const nextNode = this.cursor.getNextNode( startNode, this.isConnection, @@ -584,9 +565,7 @@ suite('Cursor', function () { assert.isNull(nextNode); }); test('Never valid - start at end - with loopback', function () { - const startNode = ASTNode.createConnectionNode( - this.blockC.nextConnection, - ); + const startNode = this.blockC.nextConnection; const nextNode = this.cursor.getNextNode( startNode, this.neverValid, @@ -595,27 +574,23 @@ suite('Cursor', function () { assert.isNull(nextNode); }); test('Always valid - start at end - with loopback', function () { - const startNode = ASTNode.createConnectionNode( - this.blockC.nextConnection, - ); + const startNode = this.blockC.nextConnection; const nextNode = this.cursor.getNextNode( startNode, this.alwaysValid, true, ); - assert.equal(nextNode.getLocation(), this.blockA.previousConnection); + assert.equal(nextNode, this.blockA.previousConnection); }); test('Valid if connection - start at end - with loopback', function () { - const startNode = ASTNode.createConnectionNode( - this.blockC.nextConnection, - ); + const startNode = this.blockC.nextConnection; const nextNode = this.cursor.getNextNode( startNode, this.isConnection, true, ); - assert.equal(nextNode.getLocation(), this.blockA.previousConnection); + assert.equal(nextNode, this.blockA.previousConnection); }); }); }); @@ -663,7 +638,7 @@ suite('Cursor', function () { this.neverValid = () => false; this.alwaysValid = () => true; this.isConnection = (node) => { - return node && node.isConnection(); + return node && node instanceof Blockly.RenderedConnection; }; }); teardown(function () { @@ -705,9 +680,7 @@ suite('Cursor', function () { this.workspace.clear(); }); test('Never valid - start at top', function () { - const startNode = ASTNode.createConnectionNode( - this.blockA.previousConnection, - ); + const startNode = this.blockA.previousConnection; const previousNode = this.cursor.getPreviousNode( startNode, this.neverValid, @@ -716,7 +689,7 @@ suite('Cursor', function () { assert.isNull(previousNode); }); test('Never valid - start in middle', function () { - const startNode = ASTNode.createBlockNode(this.blockB); + const startNode = this.blockB; const previousNode = this.cursor.getPreviousNode( startNode, this.neverValid, @@ -725,9 +698,7 @@ suite('Cursor', function () { assert.isNull(previousNode); }); test('Never valid - start at end', function () { - const startNode = ASTNode.createConnectionNode( - this.blockC.nextConnection, - ); + const startNode = this.blockC.nextConnection; const previousNode = this.cursor.getPreviousNode( startNode, this.neverValid, @@ -737,44 +708,35 @@ suite('Cursor', function () { }); test('Always valid - start at top', function () { - const startNode = ASTNode.createConnectionNode( - this.blockA.previousConnection, - ); + const startNode = this.blockA.previousConnection; const previousNode = this.cursor.getPreviousNode( startNode, this.alwaysValid, false, ); - assert.isNotNull(previousNode); + assert.isNull(previousNode); }); test('Always valid - start in middle', function () { - const startNode = ASTNode.createBlockNode(this.blockB); + const startNode = this.blockB; const previousNode = this.cursor.getPreviousNode( startNode, this.alwaysValid, false, ); - assert.equal( - previousNode.getLocation(), - this.blockB.previousConnection, - ); + assert.equal(previousNode, this.blockB.previousConnection); }); test('Always valid - start at end', function () { - const startNode = ASTNode.createConnectionNode( - this.blockC.nextConnection, - ); + const startNode = this.blockC.nextConnection; const previousNode = this.cursor.getPreviousNode( startNode, this.alwaysValid, false, ); - assert.equal(previousNode.getLocation(), this.blockC.getField('FIELD')); + assert.equal(previousNode, this.blockC.getField('FIELD')); }); test('Valid if connection - start at top', function () { - const startNode = ASTNode.createConnectionNode( - this.blockA.previousConnection, - ); + const startNode = this.blockA.previousConnection; const previousNode = this.cursor.getPreviousNode( startNode, this.isConnection, @@ -783,35 +745,25 @@ suite('Cursor', function () { assert.isNull(previousNode); }); test('Valid if connection - start in middle', function () { - const startNode = ASTNode.createBlockNode(this.blockB); + const startNode = this.blockB; const previousNode = this.cursor.getPreviousNode( startNode, this.isConnection, false, ); - assert.equal( - previousNode.getLocation(), - this.blockB.previousConnection, - ); + assert.equal(previousNode, this.blockB.previousConnection); }); test('Valid if connection - start at end', function () { - const startNode = ASTNode.createConnectionNode( - this.blockC.nextConnection, - ); + const startNode = this.blockC.nextConnection; const previousNode = this.cursor.getPreviousNode( startNode, this.isConnection, false, ); - assert.equal( - previousNode.getLocation(), - this.blockC.previousConnection, - ); + assert.equal(previousNode, this.blockC.previousConnection); }); test('Never valid - start at top - with loopback', function () { - const startNode = ASTNode.createConnectionNode( - this.blockA.previousConnection, - ); + const startNode = this.blockA.previousConnection; const previousNode = this.cursor.getPreviousNode( startNode, this.neverValid, @@ -820,27 +772,22 @@ suite('Cursor', function () { assert.isNull(previousNode); }); test('Always valid - start at top - with loopback', function () { - const startNode = ASTNode.createConnectionNode( - this.blockA.previousConnection, - ); + const startNode = this.blockA.previousConnection; const previousNode = this.cursor.getPreviousNode( startNode, this.alwaysValid, true, ); - // Previous node will be a stack node in this case. - assert.equal(previousNode.getLocation(), this.blockA); + assert.equal(previousNode, this.blockC.nextConnection); }); test('Valid if connection - start at top - with loopback', function () { - const startNode = ASTNode.createConnectionNode( - this.blockA.previousConnection, - ); + const startNode = this.blockA.previousConnection; const previousNode = this.cursor.getPreviousNode( startNode, this.isConnection, true, ); - assert.equal(previousNode.getLocation(), this.blockC.nextConnection); + assert.equal(previousNode, this.blockC.nextConnection); }); }); }); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 2b99102f645..23216e10c68 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -180,7 +180,6 @@ import {javascriptGenerator} from '../../build/javascript.loader.mjs'; // Import tests. - import './astnode_test.js'; import './block_json_test.js'; import './block_test.js'; import './clipboard_test.js'; @@ -249,6 +248,7 @@ import './metrics_test.js'; import './mutator_test.js'; import './names_test.js'; + import './navigation_test.js'; // TODO: Remove these tests. import './old_workspace_comment_test.js'; import './procedure_map_test.js'; diff --git a/tests/mocha/navigation_test.js b/tests/mocha/navigation_test.js new file mode 100644 index 00000000000..b049170b079 --- /dev/null +++ b/tests/mocha/navigation_test.js @@ -0,0 +1,567 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {assert} from '../../node_modules/chai/chai.js'; +import { + sharedTestSetup, + sharedTestTeardown, + workspaceTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Navigation', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'input_statement', + 'message0': '%1 %2 %3 %4', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'NAME', + }, + { + 'type': 'input_statement', + 'name': 'NAME', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'value_input', + 'message0': '%1', + 'args0': [ + { + 'type': 'input_value', + 'name': 'NAME', + }, + ], + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'field_input', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + ], + 'output': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + ]); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.navigator = this.workspace.getNavigator(); + const statementInput1 = this.workspace.newBlock('input_statement'); + const statementInput2 = this.workspace.newBlock('input_statement'); + const statementInput3 = this.workspace.newBlock('input_statement'); + const statementInput4 = this.workspace.newBlock('input_statement'); + const fieldWithOutput = this.workspace.newBlock('field_input'); + const valueInput = this.workspace.newBlock('value_input'); + + statementInput1.nextConnection.connect(statementInput2.previousConnection); + statementInput1.inputList[0].connection.connect( + fieldWithOutput.outputConnection, + ); + statementInput2.inputList[1].connection.connect( + statementInput3.previousConnection, + ); + + this.blocks = { + statementInput1: statementInput1, + statementInput2: statementInput2, + statementInput3: statementInput3, + statementInput4: statementInput4, + fieldWithOutput: fieldWithOutput, + valueInput: valueInput, + }; + }); + teardown(function () { + sharedTestTeardown.call(this); + }); + + suite('NavigationFunctions', function () { + setup(function () { + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'top_connection', + 'message0': '', + 'previousStatement': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'start_block', + 'message0': '', + 'nextStatement': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'fields_and_input', + 'message0': '%1 hi %2 %3 %4', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + { + 'type': 'input_dummy', + }, + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'NAME', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'two_fields', + 'message0': '%1 hi', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + ], + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'fields_and_input2', + 'message0': '%1 %2 %3 hi %4 bye', + 'args0': [ + { + 'type': 'input_value', + 'name': 'NAME', + }, + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'NAME', + }, + { + 'type': 'input_statement', + 'name': 'NAME', + }, + ], + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'dummy_input', + 'message0': 'Hello', + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'dummy_inputValue', + 'message0': 'Hello %1 %2', + 'args0': [ + { + 'type': 'input_dummy', + }, + { + 'type': 'input_value', + 'name': 'NAME', + }, + ], + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'output_next', + 'message0': '', + 'output': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + 'nextStatement': null, + }, + ]); + const noNextConnection = this.workspace.newBlock('top_connection'); + const fieldAndInputs = this.workspace.newBlock('fields_and_input'); + const twoFields = this.workspace.newBlock('two_fields'); + const fieldAndInputs2 = this.workspace.newBlock('fields_and_input2'); + const noPrevConnection = this.workspace.newBlock('start_block'); + this.blocks.noNextConnection = noNextConnection; + this.blocks.fieldAndInputs = fieldAndInputs; + this.blocks.twoFields = twoFields; + this.blocks.fieldAndInputs2 = fieldAndInputs2; + this.blocks.noPrevConnection = noPrevConnection; + + const dummyInput = this.workspace.newBlock('dummy_input'); + const dummyInputValue = this.workspace.newBlock('dummy_inputValue'); + const fieldWithOutput2 = this.workspace.newBlock('field_input'); + this.blocks.dummyInput = dummyInput; + this.blocks.dummyInputValue = dummyInputValue; + this.blocks.fieldWithOutput2 = fieldWithOutput2; + + const secondBlock = this.workspace.newBlock('input_statement'); + const outputNextBlock = this.workspace.newBlock('output_next'); + this.blocks.secondBlock = secondBlock; + this.blocks.outputNextBlock = outputNextBlock; + }); + suite('Next', function () { + setup(function () { + this.singleBlockWorkspace = new Blockly.Workspace(); + const singleBlock = this.singleBlockWorkspace.newBlock('two_fields'); + this.blocks.singleBlock = singleBlock; + }); + teardown(function () { + workspaceTeardown.call(this, this.singleBlockWorkspace); + }); + + test('fromPreviousToBlock', function () { + const prevConnection = this.blocks.statementInput1.previousConnection; + const nextNode = this.navigator.getNextSibling(prevConnection); + assert.equal(nextNode, this.blocks.statementInput1); + }); + test('fromBlockToNext', function () { + const nextConnection = this.blocks.statementInput1.nextConnection; + const nextNode = this.navigator.getNextSibling( + this.blocks.statementInput1, + ); + assert.equal(nextNode, nextConnection); + }); + test('fromNextToPrevious', function () { + const nextConnection = this.blocks.statementInput1.nextConnection; + const prevConnection = this.blocks.statementInput2.previousConnection; + const nextNode = this.navigator.getNextSibling(nextConnection); + assert.equal(nextNode, prevConnection); + }); + test('fromInputToInput', function () { + const input = this.blocks.statementInput1.inputList[0]; + const inputConnection = + this.blocks.statementInput1.inputList[1].connection; + const nextNode = this.navigator.getNextSibling(input.connection); + assert.equal(nextNode, inputConnection); + }); + test('fromInputToStatementInput', function () { + const input = this.blocks.fieldAndInputs2.inputList[1]; + const inputConnection = + this.blocks.fieldAndInputs2.inputList[2].connection; + const nextNode = this.navigator.getNextSibling(input.connection); + assert.equal(nextNode, inputConnection); + }); + test('fromInputToField', function () { + const input = this.blocks.fieldAndInputs2.inputList[0]; + const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0]; + const nextNode = this.navigator.getNextSibling(input.connection); + assert.equal(nextNode, field); + }); + test('fromInputToNull', function () { + const input = this.blocks.fieldAndInputs2.inputList[2]; + const nextNode = this.navigator.getNextSibling(input.connection); + assert.isNull(nextNode); + }); + test('fromOutputToBlock', function () { + const output = this.blocks.fieldWithOutput.outputConnection; + const nextNode = this.navigator.getNextSibling(output); + assert.equal(nextNode, this.blocks.fieldWithOutput); + }); + test('fromFieldToInput', function () { + const field = this.blocks.statementInput1.inputList[0].fieldRow[1]; + const inputConnection = + this.blocks.statementInput1.inputList[0].connection; + const nextNode = this.navigator.getNextSibling(field); + assert.equal(nextNode, inputConnection); + }); + test('fromFieldToField', function () { + const field = this.blocks.fieldAndInputs.inputList[0].fieldRow[0]; + const field2 = this.blocks.fieldAndInputs.inputList[1].fieldRow[0]; + const nextNode = this.navigator.getNextSibling(field); + assert.equal(nextNode, field2); + }); + test('fromFieldToNull', function () { + const field = this.blocks.twoFields.inputList[0].fieldRow[0]; + const nextNode = this.navigator.getNextSibling(field); + assert.isNull(nextNode); + }); + }); + + suite('Previous', function () { + test('fromPreviousToNext', function () { + const prevConnection = this.blocks.statementInput2.previousConnection; + const prevNode = this.navigator.getPreviousSibling(prevConnection); + const nextConnection = this.blocks.statementInput1.nextConnection; + assert.equal(prevNode, nextConnection); + }); + test('fromPreviousToInput', function () { + const prevConnection = this.blocks.statementInput3.previousConnection; + const prevNode = this.navigator.getPreviousSibling(prevConnection); + assert.isNull(prevNode); + }); + test('fromBlockToPrevious', function () { + const prevNode = this.navigator.getPreviousSibling( + this.blocks.statementInput1, + ); + const prevConnection = this.blocks.statementInput1.previousConnection; + assert.equal(prevNode, prevConnection); + }); + test('fromBlockToOutput', function () { + const prevNode = this.navigator.getPreviousSibling( + this.blocks.fieldWithOutput, + ); + const outputConnection = this.blocks.fieldWithOutput.outputConnection; + assert.equal(prevNode, outputConnection); + }); + test('fromNextToBlock', function () { + const nextConnection = this.blocks.statementInput1.nextConnection; + const prevNode = this.navigator.getPreviousSibling(nextConnection); + assert.equal(prevNode, this.blocks.statementInput1); + }); + test('fromInputToField', function () { + const input = this.blocks.statementInput1.inputList[0]; + const prevNode = this.navigator.getPreviousSibling(input.connection); + assert.equal(prevNode, input.fieldRow[1]); + }); + test('fromInputToNull', function () { + const input = this.blocks.fieldAndInputs2.inputList[0]; + const prevNode = this.navigator.getPreviousSibling(input.connection); + assert.isNull(prevNode); + }); + test('fromInputToInput', function () { + const input = this.blocks.fieldAndInputs2.inputList[2]; + const inputConnection = + this.blocks.fieldAndInputs2.inputList[1].connection; + const prevNode = this.navigator.getPreviousSibling(input.connection); + assert.equal(prevNode, inputConnection); + }); + test('fromOutputToNull', function () { + const output = this.blocks.fieldWithOutput.outputConnection; + const prevNode = this.navigator.getPreviousSibling(output); + assert.isNull(prevNode); + }); + test('fromFieldToNull', function () { + const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; + const prevNode = this.navigator.getPreviousSibling(field); + assert.isNull(prevNode); + }); + test('fromFieldToInput', function () { + const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0]; + const inputConnection = + this.blocks.fieldAndInputs2.inputList[0].connection; + const prevNode = this.navigator.getPreviousSibling(field); + assert.equal(prevNode, inputConnection); + }); + test('fromFieldToField', function () { + const field = this.blocks.fieldAndInputs.inputList[1].fieldRow[0]; + const field2 = this.blocks.fieldAndInputs.inputList[0].fieldRow[0]; + const prevNode = this.navigator.getPreviousSibling(field); + assert.equal(prevNode, field2); + }); + }); + + suite('In', function () { + setup(function () { + this.emptyWorkspace = Blockly.inject(document.createElement('div'), {}); + }); + teardown(function () { + workspaceTeardown.call(this, this.emptyWorkspace); + }); + + test('fromInputToOutput', function () { + const input = this.blocks.statementInput1.inputList[0]; + const inNode = this.navigator.getFirstChild(input.connection); + const outputConnection = this.blocks.fieldWithOutput.outputConnection; + assert.equal(inNode, outputConnection); + }); + test('fromInputToNull', function () { + const input = this.blocks.statementInput2.inputList[0]; + const inNode = this.navigator.getFirstChild(input.connection); + assert.isNull(inNode); + }); + test('fromInputToPrevious', function () { + const input = this.blocks.statementInput2.inputList[1]; + const previousConnection = + this.blocks.statementInput3.previousConnection; + const inNode = this.navigator.getFirstChild(input.connection); + assert.equal(inNode, previousConnection); + }); + test('fromBlockToInput', function () { + const input = this.blocks.valueInput.inputList[0]; + const inNode = this.navigator.getFirstChild(this.blocks.valueInput); + assert.equal(inNode, input.connection); + }); + test('fromBlockToField', function () { + const inNode = this.navigator.getFirstChild( + this.blocks.statementInput1, + ); + const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; + assert.equal(inNode, field); + }); + test('fromBlockToNull_DummyInput', function () { + const inNode = this.navigator.getFirstChild(this.blocks.dummyInput); + assert.isNull(inNode); + }); + test('fromBlockToInput_DummyInputValue', function () { + const inputConnection = + this.blocks.dummyInputValue.inputList[1].connection; + const inNode = this.navigator.getFirstChild( + this.blocks.dummyInputValue, + ); + assert.equal(inNode, inputConnection); + }); + test('fromOuputToNull', function () { + const output = this.blocks.fieldWithOutput.outputConnection; + const inNode = this.navigator.getFirstChild(output); + assert.isNull(inNode); + }); + test('fromFieldToNull', function () { + const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; + const inNode = this.navigator.getFirstChild(field); + assert.isNull(inNode); + }); + test('fromWorkspaceToBlock', function () { + const inNode = this.navigator.getFirstChild(this.workspace); + assert.equal(inNode, this.workspace.getTopBlocks(true)[0]); + }); + test('fromWorkspaceToNull', function () { + const inNode = this.navigator.getFirstChild(this.emptyWorkspace); + assert.isNull(inNode); + }); + }); + + suite('Out', function () { + setup(function () { + const secondBlock = this.blocks.secondBlock; + const outputNextBlock = this.blocks.outputNextBlock; + this.blocks.noPrevConnection.nextConnection.connect( + secondBlock.previousConnection, + ); + secondBlock.inputList[0].connection.connect( + outputNextBlock.outputConnection, + ); + }); + + test('fromInputToBlock', function () { + const input = this.blocks.statementInput1.inputList[0]; + const outNode = this.navigator.getParent(input.connection); + assert.equal(outNode, this.blocks.statementInput1); + }); + test('fromOutputToInput', function () { + const output = this.blocks.fieldWithOutput.outputConnection; + const outNode = this.navigator.getParent(output); + assert.equal( + outNode, + this.blocks.statementInput1.inputList[0].connection, + ); + }); + test('fromOutputToBlock', function () { + const output = this.blocks.fieldWithOutput2.outputConnection; + const outNode = this.navigator.getParent(output); + assert.equal(outNode, this.blocks.fieldWithOutput2); + }); + test('fromFieldToBlock', function () { + const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; + const outNode = this.navigator.getParent(field); + assert.equal(outNode, this.blocks.statementInput1); + }); + test('fromPreviousToInput', function () { + const previous = this.blocks.statementInput3.previousConnection; + const inputConnection = + this.blocks.statementInput2.inputList[1].connection; + const outNode = this.navigator.getParent(previous); + assert.equal(outNode, inputConnection); + }); + test('fromPreviousToBlock', function () { + const previous = this.blocks.statementInput2.previousConnection; + const outNode = this.navigator.getParent(previous); + assert.equal(outNode, this.blocks.statementInput1); + }); + test('fromNextToInput', function () { + const next = this.blocks.statementInput3.nextConnection; + const inputConnection = + this.blocks.statementInput2.inputList[1].connection; + const outNode = this.navigator.getParent(next); + assert.equal(outNode, inputConnection); + }); + test('fromNextToBlock', function () { + const next = this.blocks.statementInput2.nextConnection; + const outNode = this.navigator.getParent(next); + assert.equal(outNode, this.blocks.statementInput1); + }); + test('fromNextToBlock_NoPreviousConnection', function () { + const next = this.blocks.secondBlock.nextConnection; + const outNode = this.navigator.getParent(next); + assert.equal(outNode, this.blocks.noPrevConnection); + }); + /** + * This is where there is a block with both an output connection and a + * next connection attached to an input. + */ + test('fromNextToInput_OutputAndPreviousConnection', function () { + const next = this.blocks.outputNextBlock.nextConnection; + const outNode = this.navigator.getParent(next); + assert.equal(outNode, this.blocks.secondBlock.inputList[0].connection); + }); + test('fromBlockToStack', function () { + const outNode = this.navigator.getParent(this.blocks.statementInput2); + assert.equal(outNode, this.blocks.statementInput1); + }); + test('fromBlockToInput', function () { + const input = this.blocks.statementInput2.inputList[1].connection; + const outNode = this.navigator.getParent(this.blocks.statementInput3); + assert.equal(outNode, input); + }); + test('fromTopBlockToStack', function () { + const outNode = this.navigator.getParent(this.blocks.statementInput1); + assert.equal(outNode, this.blocks.statementInput1); + }); + test('fromBlockToStack_OutputConnection', function () { + const outNode = this.navigator.getParent(this.blocks.fieldWithOutput2); + assert.equal(outNode, this.blocks.fieldWithOutput2); + }); + test('fromBlockToInput_OutputConnection', function () { + const inputConnection = this.blocks.secondBlock.inputList[0].connection; + const outNode = this.navigator.getParent(this.blocks.outputNextBlock); + assert.equal(outNode, inputConnection); + }); + }); + }); +}); diff --git a/tests/typescript/src/field/different_user_input.ts b/tests/typescript/src/field/different_user_input.ts index f91f25bbc86..3083fe5545e 100644 --- a/tests/typescript/src/field/different_user_input.ts +++ b/tests/typescript/src/field/different_user_input.ts @@ -61,6 +61,10 @@ class FieldMitosis extends Field { }); this.value_ = {cells}; } + + getClass() { + return FieldMitosis; + } } fieldRegistry.register('field_mitosis', FieldMitosis); From bb76d6e12c92230dce4ca5f9c3842c7739788f8e Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Wed, 7 May 2025 09:28:51 -0700 Subject: [PATCH 190/222] fix!: remove MarkerSvg and uses (#8991) * fix: delete MarkerSvg (marker drawer) * fix: delete marker and cursor SVG elements * chore: format * chore: lint --- core/block_svg.ts | 40 -- core/field.ts | 71 -- core/flyout_button.ts | 9 - core/interfaces/i_ast_node_location_svg.ts | 16 +- core/keyboard_nav/line_cursor.ts | 156 ----- core/keyboard_nav/marker.ts | 44 -- core/marker_manager.ts | 71 -- core/renderers/common/block_rendering.ts | 2 - core/renderers/common/i_path_object.ts | 28 - core/renderers/common/marker_svg.ts | 767 --------------------- core/renderers/common/path_object.ts | 48 -- core/renderers/common/renderer.ts | 14 - core/renderers/zelos/marker_svg.ts | 144 ---- core/renderers/zelos/renderer.ts | 17 - core/renderers/zelos/zelos.ts | 2 - core/workspace_svg.ts | 27 - 16 files changed, 1 insertion(+), 1455 deletions(-) delete mode 100644 core/renderers/common/marker_svg.ts delete mode 100644 core/renderers/zelos/marker_svg.ts diff --git a/core/block_svg.ts b/core/block_svg.ts index 06cb7e1a233..a4171b82635 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -50,7 +50,6 @@ import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {IIcon} from './interfaces/i_icon.js'; import type {INavigable} from './interfaces/i_navigable.js'; import * as internalConstants from './internal_constants.js'; -import {MarkerManager} from './marker_manager.js'; import {Msg} from './msg.js'; import * as renderManagement from './render_management.js'; import {RenderedConnection} from './rendered_connection.js'; @@ -1679,7 +1678,6 @@ export class BlockSvg this.tightenChildrenEfficiently(); dom.stopTextWidthCache(); - this.updateMarkers_(); } /** @@ -1699,44 +1697,6 @@ export class BlockSvg if (this.nextConnection) this.nextConnection.tightenEfficiently(); } - /** Redraw any attached marker or cursor svgs if needed. */ - protected updateMarkers_() { - if (this.workspace.keyboardAccessibilityMode && this.pathObject.cursorSvg) { - this.workspace.getCursor()!.draw(); - } - if (this.workspace.keyboardAccessibilityMode && this.pathObject.markerSvg) { - // TODO(#4592): Update all markers on the block. - this.workspace.getMarker(MarkerManager.LOCAL_MARKER)!.draw(); - } - for (const input of this.inputList) { - for (const field of input.fieldRow) { - field.updateMarkers_(); - } - } - } - - /** - * Add the cursor SVG to this block's SVG group. - * - * @param cursorSvg The SVG root of the cursor to be added to the block SVG - * group. - * @internal - */ - setCursorSvg(cursorSvg: SVGElement) { - this.pathObject.setCursorSvg(cursorSvg); - } - - /** - * Add the marker SVG to this block's SVG group. - * - * @param markerSvg The SVG root of the marker to be added to the block SVG - * group. - * @internal - */ - setMarkerSvg(markerSvg: SVGElement) { - this.pathObject.setMarkerSvg(markerSvg); - } - /** * Returns a bounding box describing the dimensions of this block * and any blocks stacked below it. diff --git a/core/field.ts b/core/field.ts index c89a53de51e..26fd44df96f 100644 --- a/core/field.ts +++ b/core/field.ts @@ -31,7 +31,6 @@ import type {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js'; import type {INavigable} from './interfaces/i_navigable.js'; import type {IRegistrable} from './interfaces/i_registrable.js'; import {ISerializable} from './interfaces/i_serializable.js'; -import {MarkerManager} from './marker_manager.js'; import type {ConstantProvider} from './renderers/common/constants.js'; import type {KeyboardShortcut} from './shortcut_registry.js'; import * as Tooltip from './tooltip.js'; @@ -113,18 +112,6 @@ export abstract class Field private tooltip: Tooltip.TipInfo | null = null; protected size_: Size; - /** - * Holds the cursors svg element when the cursor is attached to the field. - * This is null if there is no cursor on the field. - */ - private cursorSvg: SVGElement | null = null; - - /** - * Holds the markers svg element when the marker is attached to the field. - * This is null if there is no marker on the field. - */ - private markerSvg: SVGElement | null = null; - /** The rendered field's SVG group element. */ protected fieldGroup_: SVGGElement | null = null; @@ -1358,64 +1345,6 @@ export abstract class Field return false; } - /** - * Add the cursor SVG to this fields SVG group. - * - * @param cursorSvg The SVG root of the cursor to be added to the field group. - * @internal - */ - setCursorSvg(cursorSvg: SVGElement) { - if (!cursorSvg) { - this.cursorSvg = null; - return; - } - - if (!this.fieldGroup_) { - throw new Error(`The field group is ${this.fieldGroup_}.`); - } - this.fieldGroup_.appendChild(cursorSvg); - this.cursorSvg = cursorSvg; - } - - /** - * Add the marker SVG to this fields SVG group. - * - * @param markerSvg The SVG root of the marker to be added to the field group. - * @internal - */ - setMarkerSvg(markerSvg: SVGElement) { - if (!markerSvg) { - this.markerSvg = null; - return; - } - - if (!this.fieldGroup_) { - throw new Error(`The field group is ${this.fieldGroup_}.`); - } - this.fieldGroup_.appendChild(markerSvg); - this.markerSvg = markerSvg; - } - - /** - * Redraw any attached marker or cursor svgs if needed. - * - * @internal - */ - updateMarkers_() { - const block = this.getSourceBlock(); - if (!block) { - throw new UnattachedFieldError(); - } - const workspace = block.workspace as WorkspaceSvg; - if (workspace.keyboardAccessibilityMode && this.cursorSvg) { - workspace.getCursor()!.draw(); - } - if (workspace.keyboardAccessibilityMode && this.markerSvg) { - // TODO(#4592): Update all markers on the field. - workspace.getMarker(MarkerManager.LOCAL_MARKER)!.draw(); - } - } - /** See IFocusableNode.getFocusableElement. */ getFocusableElement(): HTMLElement | SVGElement { if (!this.fieldGroup_) { diff --git a/core/flyout_button.ts b/core/flyout_button.ts index 2b314ed2ebc..257a90bba20 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -348,15 +348,6 @@ export class FlyoutButton } } - /** - * Required by IASTNodeLocationSvg, but not used. A marker cannot be set on a - * button. If the 'mark' shortcut is used on a button, its associated callback - * function is triggered. - */ - setMarkerSvg() { - throw new Error('Attempted to set a marker on a button.'); - } - /** * Do something when the button is clicked. * diff --git a/core/interfaces/i_ast_node_location_svg.ts b/core/interfaces/i_ast_node_location_svg.ts index 729e5f09543..60a871e053b 100644 --- a/core/interfaces/i_ast_node_location_svg.ts +++ b/core/interfaces/i_ast_node_location_svg.ts @@ -11,18 +11,4 @@ import type {IASTNodeLocation} from './i_ast_node_location.js'; /** * An AST node location SVG interface. */ -export interface IASTNodeLocationSvg extends IASTNodeLocation { - /** - * Add the marker SVG to this node's SVG group. - * - * @param markerSvg The SVG root of the marker to be added to the SVG group. - */ - setMarkerSvg(markerSvg: SVGElement | null): void; - - /** - * Add the cursor SVG to this node's SVG group. - * - * @param cursorSvg The SVG root of the cursor to be added to the SVG group. - */ - setCursorSvg(cursorSvg: SVGElement | null): void; -} +export interface IASTNodeLocationSvg extends IASTNodeLocation {} diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 67adade56fc..44c9c0d3887 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -30,12 +30,8 @@ import {isFocusableNode} from '../interfaces/i_focusable_node.js'; import type {INavigable} from '../interfaces/i_navigable.js'; import * as registry from '../registry.js'; import {RenderedConnection} from '../rendered_connection.js'; -import type {MarkerSvg} from '../renderers/common/marker_svg.js'; -import type {PathObject} from '../renderers/zelos/path_object.js'; import {Renderer} from '../renderers/zelos/renderer.js'; -import * as dom from '../utils/dom.js'; import {WorkspaceSvg} from '../workspace_svg.js'; -import {ASTNode} from './ast_node.js'; import {BlockNavigationPolicy} from './block_navigation_policy.js'; import {ConnectionNavigationPolicy} from './connection_navigation_policy.js'; import {FieldNavigationPolicy} from './field_navigation_policy.js'; @@ -574,40 +570,6 @@ export class LineCursor extends Marker { return super.getCurNode(); } - /** - * 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: 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. * @@ -631,124 +593,6 @@ export class LineCursor extends Marker { } } - /** - * Draw this cursor's marker. - * - * This is a wrapper around this.drawer.draw (usually implemented by - * MarkerSvg.prototype.draw) that will, if newNode is a BLOCK node, - * instead call `setSelected` to select it (if it's a regular block) - * or `addSelect` (if it's a shadow block, since shadow blocks can't - * be selected) instead of using the normal drawer logic. - * - * TODO(#142): The selection and fake-selection code was originally - * a hack added for testing on October 28 2024, because the default - * drawer (MarkerSvg) behaviour in Zelos was to draw a box around - * the block and all attached child blocks, which was confusing when - * navigating stacks. - * - * Since then we have decided that we probably _do_ in most cases - * want navigating to a block to select the block, but more - * particularly that we want navigation to move _focus_. Replace - * this selection hack with non-hacky changing of focus once that's - * possible. - * - * @param oldNode The previous node. - * @param curNode The current node. - * @param realDrawer The object ~in charge of drawing the marker. - */ - private drawMarker( - oldNode: ASTNode | null, - curNode: ASTNode | null, - realDrawer: MarkerSvg, - ) { - // If old node was a block, unselect it or remove fake selection. - if (oldNode?.getType() === ASTNode.types.BLOCK) { - const block = oldNode.getLocation() as BlockSvg; - if (!block.isShadow()) { - // Selection should already be in sync. - } else { - block.removeSelect(); - } - } - - if (this.isZelos && oldNode && this.isValueInputConnection(oldNode)) { - this.hideAtInput(oldNode); - } - - const curNodeType = curNode?.getType(); - const isZelosInputConnection = - this.isZelos && curNode && this.isValueInputConnection(curNode); - - // If drawing can't be handled locally, just use the drawer. - if (curNodeType !== ASTNode.types.BLOCK && !isZelosInputConnection) { - realDrawer.draw(oldNode, curNode); - return; - } - - // Hide any visible marker SVG and instead do some manual rendering. - realDrawer.hide(); - - if (isZelosInputConnection) { - this.showAtInput(curNode); - } else if (curNode && curNodeType === ASTNode.types.BLOCK) { - const block = curNode.getLocation() as BlockSvg; - if (!block.isShadow()) { - // Selection should already be in sync. - } else { - block.addSelect(); - block.getParent()?.removeSelect(); - } - } - - // Call MarkerSvg.prototype.fireMarkerEvent like - // MarkerSvg.prototype.draw would (even though it's private). - (realDrawer as any)?.fireMarkerEvent?.(oldNode, curNode); - } - - /** - * Check whether the node represents a value input connection. - * - * @param node The node to check - * @returns True if the node represents a value input connection. - */ - private isValueInputConnection(node: ASTNode) { - if (node?.getType() !== ASTNode.types.INPUT) return false; - const connection = node.getLocation() as RenderedConnection; - return connection.type === ConnectionType.INPUT_VALUE; - } - - /** - * Hide the cursor rendering at the given input node. - * - * @param node The input node to hide. - */ - private hideAtInput(node: ASTNode) { - const inputConnection = node.getLocation() as RenderedConnection; - const sourceBlock = inputConnection.getSourceBlock() as BlockSvg; - const input = inputConnection.getParentInput(); - if (input) { - const pathObject = sourceBlock.pathObject as PathObject; - const outlinePath = pathObject.getOutlinePath(input.name); - dom.removeClass(outlinePath, 'inputActiveFocus'); - } - } - - /** - * Show the cursor rendering at the given input node. - * - * @param node The input node to show. - */ - private showAtInput(node: ASTNode) { - const inputConnection = node.getLocation() as RenderedConnection; - const sourceBlock = inputConnection.getSourceBlock() as BlockSvg; - const input = inputConnection.getParentInput(); - if (input) { - const pathObject = sourceBlock.pathObject as PathObject; - const outlinePath = pathObject.getOutlinePath(input.name); - dom.addClass(outlinePath, 'inputActiveFocus'); - } - } - /** * Updates the current node to match the selection. * diff --git a/core/keyboard_nav/marker.ts b/core/keyboard_nav/marker.ts index 7a810dfe6c1..759ae19a7af 100644 --- a/core/keyboard_nav/marker.ts +++ b/core/keyboard_nav/marker.ts @@ -17,7 +17,6 @@ import {Field} from '../field.js'; import {FlyoutButton} from '../flyout_button.js'; import type {INavigable} from '../interfaces/i_navigable.js'; import {RenderedConnection} from '../rendered_connection.js'; -import type {MarkerSvg} from '../renderers/common/marker_svg.js'; import {Coordinate} from '../utils/coordinate.js'; import {WorkspaceSvg} from '../workspace_svg.js'; import {ASTNode} from './ast_node.js'; @@ -33,33 +32,9 @@ export class Marker { /** The current location of the marker. */ protected curNode: INavigable | null = null; - /** - * The object in charge of drawing the visual representation of the current - * node. - */ - private drawer: MarkerSvg | null = null; - /** The type of the marker. */ type = 'marker'; - /** - * Sets the object in charge of drawing the marker. - * - * @param drawer The object in charge of drawing the marker. - */ - setDrawer(drawer: MarkerSvg) { - this.drawer = drawer; - } - - /** - * Get the current drawer for the marker. - * - * @returns The object in charge of drawing the marker. - */ - getDrawer(): MarkerSvg | null { - return this.drawer; - } - /** * Gets the current location of the marker. * @@ -75,30 +50,11 @@ export class Marker { * @param newNode The new location of the marker, or null to remove it. */ setCurNode(newNode: INavigable | null) { - const oldNode = this.curNode; this.curNode = newNode; - this.drawer?.draw(this.toASTNode(oldNode), this.toASTNode(this.curNode)); - } - - /** - * Redraw the current marker. - * - * @internal - */ - draw() { - const node = this.toASTNode(this.curNode); - this.drawer?.draw(node, node); - } - - /** Hide the marker SVG. */ - hide() { - this.drawer?.hide(); } /** Dispose of this marker. */ dispose() { - this.drawer?.dispose(); - this.drawer = null; this.curNode = null; } diff --git a/core/marker_manager.ts b/core/marker_manager.ts index a8d1d20c2bd..95a2d9b8bce 100644 --- a/core/marker_manager.ts +++ b/core/marker_manager.ts @@ -25,15 +25,9 @@ export class MarkerManager { /** The cursor. */ private cursor: LineCursor | null = null; - /** The cursor's SVG element. */ - private cursorSvg: SVGElement | null = null; - /** The map of markers for the workspace. */ private markers = new Map(); - /** The marker's SVG element. */ - private markerSvg: SVGElement | null = null; - /** * @param workspace The workspace for the marker manager. * @internal @@ -50,11 +44,6 @@ export class MarkerManager { if (this.markers.has(id)) { this.unregisterMarker(id); } - const drawer = this.workspace - .getRenderer() - .makeMarkerDrawer(this.workspace, marker); - marker.setDrawer(drawer); - this.setMarkerSvg(drawer.createDom()); this.markers.set(id, marker); } @@ -105,67 +94,7 @@ export class MarkerManager { * @param cursor The cursor used to move around this workspace. */ setCursor(cursor: LineCursor) { - this.cursor?.getDrawer()?.dispose(); this.cursor = cursor; - if (this.cursor) { - const drawer = this.workspace - .getRenderer() - .makeMarkerDrawer(this.workspace, this.cursor); - this.cursor.setDrawer(drawer); - this.setCursorSvg(drawer.createDom()); - } - } - - /** - * Add the cursor SVG to this workspace SVG group. - * - * @param cursorSvg The SVG root of the cursor to be added to the workspace - * SVG group. - * @internal - */ - setCursorSvg(cursorSvg: SVGElement | null) { - if (!cursorSvg) { - this.cursorSvg = null; - return; - } - - this.workspace.getBlockCanvas()!.appendChild(cursorSvg); - this.cursorSvg = cursorSvg; - } - - /** - * Add the marker SVG to this workspaces SVG group. - * - * @param markerSvg The SVG root of the marker to be added to the workspace - * SVG group. - * @internal - */ - setMarkerSvg(markerSvg: SVGElement | null) { - if (!markerSvg) { - this.markerSvg = null; - return; - } - - if (this.workspace.getBlockCanvas()) { - if (this.cursorSvg) { - this.workspace - .getBlockCanvas()! - .insertBefore(markerSvg, this.cursorSvg); - } else { - this.workspace.getBlockCanvas()!.appendChild(markerSvg); - } - } - } - - /** - * Redraw the attached cursor SVG if needed. - * - * @internal - */ - updateMarkers() { - if (this.workspace.keyboardAccessibilityMode && this.cursorSvg) { - this.workspace.getCursor()!.draw(); - } } /** diff --git a/core/renderers/common/block_rendering.ts b/core/renderers/common/block_rendering.ts index 27fbbd538c7..7ac27ae8b73 100644 --- a/core/renderers/common/block_rendering.ts +++ b/core/renderers/common/block_rendering.ts @@ -33,7 +33,6 @@ import {Types} from '../measurables/types.js'; import {Drawer} from './drawer.js'; import type {IPathObject} from './i_path_object.js'; import {RenderInfo} from './info.js'; -import {MarkerSvg} from './marker_svg.js'; import {PathObject} from './path_object.js'; import {Renderer} from './renderer.js'; @@ -94,7 +93,6 @@ export { InRowSpacer, IPathObject, JaggedEdge, - MarkerSvg, Measurable, NextConnection, OutputConnection, diff --git a/core/renderers/common/i_path_object.ts b/core/renderers/common/i_path_object.ts index 776ba0067ea..a68c3a41148 100644 --- a/core/renderers/common/i_path_object.ts +++ b/core/renderers/common/i_path_object.ts @@ -30,18 +30,6 @@ export interface IPathObject { /** The primary path of the block. */ style: BlockStyle; - /** - * Holds the cursors SVG element when the cursor is attached to the block. - * This is null if there is no cursor on the block. - */ - cursorSvg: SVGElement | null; - - /** - * Holds the markers SVG element when the marker is attached to the block. - * This is null if there is no marker on the block. - */ - markerSvg: SVGElement | null; - /** * Set the path generated by the renderer onto the respective SVG element. * @@ -54,22 +42,6 @@ export interface IPathObject { */ flipRTL(): void; - /** - * Add the cursor SVG to this block's SVG group. - * - * @param cursorSvg The SVG root of the cursor to be added to the block SVG - * group. - */ - setCursorSvg(cursorSvg: SVGElement): void; - - /** - * Add the marker SVG to this block's SVG group. - * - * @param markerSvg The SVG root of the marker to be added to the block SVG - * group. - */ - setMarkerSvg(markerSvg: SVGElement): void; - /** * Set whether the block shows a highlight or not. Block highlighting is * often used to visually mark blocks currently being executed. diff --git a/core/renderers/common/marker_svg.ts b/core/renderers/common/marker_svg.ts deleted file mode 100644 index 4805e70400a..00000000000 --- a/core/renderers/common/marker_svg.ts +++ /dev/null @@ -1,767 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -// Former goog.module ID: Blockly.blockRendering.MarkerSvg - -// Unused import preserved for side-effects. Remove if unneeded. -import '../../events/events_marker_move.js'; - -import type {BlockSvg} from '../../block_svg.js'; -import type {Connection} from '../../connection.js'; -import {ConnectionType} from '../../connection_type.js'; -import {EventType} from '../../events/type.js'; -import * as eventUtils from '../../events/utils.js'; -import type {Field} from '../../field.js'; -import {FlyoutButton} from '../../flyout_button.js'; -import type {IASTNodeLocationSvg} from '../../interfaces/i_ast_node_location_svg.js'; -import {ASTNode} from '../../keyboard_nav/ast_node.js'; -import type {Marker} from '../../keyboard_nav/marker.js'; -import type {RenderedConnection} from '../../rendered_connection.js'; -import * as dom from '../../utils/dom.js'; -import {Svg} from '../../utils/svg.js'; -import * as svgPaths from '../../utils/svg_paths.js'; -import type {WorkspaceSvg} from '../../workspace_svg.js'; -import type {ConstantProvider, Notch, PuzzleTab} from './constants.js'; - -/** The name of the CSS class for a cursor. */ -const CURSOR_CLASS = 'blocklyCursor'; - -/** The name of the CSS class for a marker. */ -const MARKER_CLASS = 'blocklyMarker'; - -/** - * What we multiply the height by to get the height of the marker. - * Only used for the block and block connections. - */ -const HEIGHT_MULTIPLIER = 3 / 4; - -/** - * Class for a marker, containing methods for graphically rendering a marker as - * SVG. - */ -export class MarkerSvg { - /** - * The workspace, field, or block that the marker SVG element should be - * attached to. - */ - protected parent: IASTNodeLocationSvg | null = null; - - /** The current SVG element for the marker. */ - currentMarkerSvg: SVGElement | null = null; - colour_: string; - - /** The root SVG group containing the marker. */ - protected markerSvg_: SVGGElement | null = null; - protected svgGroup_: SVGGElement | null = null; - - protected markerBlock_: SVGPathElement | null = null; - - protected markerInput_: SVGPathElement | null = null; - protected markerSvgLine_: SVGRectElement | null = null; - - protected markerSvgRect_: SVGRectElement | null = null; - - /** The constants necessary to draw the marker. */ - protected constants_: ConstantProvider; - - /** - * @param workspace The workspace the marker belongs to. - * @param constants The constants for the renderer. - * @param marker The marker to draw. - */ - constructor( - protected readonly workspace: WorkspaceSvg, - constants: ConstantProvider, - protected readonly marker: Marker, - ) { - this.constants_ = constants; - - const defaultColour = this.isCursor() - ? this.constants_.CURSOR_COLOUR - : this.constants_.MARKER_COLOUR; - - /** The colour of the marker. */ - this.colour_ = marker.colour || defaultColour; - } - - /** - * Return the root node of the SVG or null if none exists. - * - * @returns The root SVG node. - */ - getSvgRoot(): SVGElement | null { - return this.svgGroup_; - } - - /** - * Get the marker. - * - * @returns The marker to draw for. - */ - getMarker(): Marker { - return this.marker; - } - - /** - * True if the marker should be drawn as a cursor, false otherwise. - * A cursor is drawn as a flashing line. A marker is drawn as a solid line. - * - * @returns True if the marker is a cursor, false otherwise. - */ - isCursor(): boolean { - return this.marker.type === 'cursor'; - } - - /** - * Create the DOM element for the marker. - * - * @returns The marker controls SVG group. - */ - createDom(): SVGElement { - const className = this.isCursor() ? CURSOR_CLASS : MARKER_CLASS; - - this.svgGroup_ = dom.createSvgElement(Svg.G, {'class': className}); - - this.createDomInternal_(); - return this.svgGroup_; - } - - /** - * Attaches the SVG root of the marker to the SVG group of the parent. - * - * @param newParent The workspace, field, or block that the marker SVG element - * should be attached to. - */ - protected setParent_(newParent: IASTNodeLocationSvg) { - if (!this.isCursor()) { - if (this.parent) { - this.parent.setMarkerSvg(null); - } - newParent.setMarkerSvg(this.getSvgRoot()); - } else { - if (this.parent) { - this.parent.setCursorSvg(null); - } - newParent.setCursorSvg(this.getSvgRoot()); - } - this.parent = newParent; - } - - /** - * Update the marker. - * - * @param oldNode The previous node the marker was on or null. - * @param curNode The node that we want to draw the marker for. - */ - draw(oldNode: ASTNode | null, curNode: ASTNode | null) { - if (!curNode) { - this.hide(); - return; - } - - this.constants_ = this.workspace.getRenderer().getConstants(); - - const defaultColour = this.isCursor() - ? this.constants_.CURSOR_COLOUR - : this.constants_.MARKER_COLOUR; - this.colour_ = this.marker.colour || defaultColour; - this.applyColour_(curNode); - - this.showAtLocation_(curNode); - - this.fireMarkerEvent(oldNode, curNode); - - // Ensures the marker will be visible immediately after the move. - const animate = this.currentMarkerSvg!.childNodes[0]; - if ( - animate !== undefined && - (animate as SVGAnimationElement).beginElement - ) { - (animate as SVGAnimationElement).beginElement(); - } - } - - /** - * Update the marker's visible state based on the type of curNode.. - * - * @param curNode The node that we want to draw the marker for. - */ - protected showAtLocation_(curNode: ASTNode) { - const curNodeAsConnection = curNode.getLocation() as Connection; - const connectionType = curNodeAsConnection.type; - if (curNode.getType() === ASTNode.types.BLOCK) { - this.showWithBlock_(curNode); - } else if (curNode.getType() === ASTNode.types.OUTPUT) { - this.showWithOutput_(curNode); - } else if (connectionType === ConnectionType.INPUT_VALUE) { - this.showWithInput_(curNode); - } else if (connectionType === ConnectionType.NEXT_STATEMENT) { - this.showWithNext_(curNode); - } else if (curNode.getType() === ASTNode.types.PREVIOUS) { - this.showWithPrevious_(curNode); - } else if (curNode.getType() === ASTNode.types.FIELD) { - this.showWithField_(curNode); - } else if (curNode.getType() === ASTNode.types.WORKSPACE) { - this.showWithCoordinates_(curNode); - } else if (curNode.getType() === ASTNode.types.STACK) { - this.showWithStack_(curNode); - } else if (curNode.getType() === ASTNode.types.BUTTON) { - this.showWithButton_(curNode); - } - } - - /************************** - * Display - **************************/ - - /** - * Show the marker as a combination of the previous connection and block, - * the output connection and block, or just the block. - * - * @param curNode The node to draw the marker for. - */ - protected showWithBlockPrevOutput(curNode: ASTNode) { - const block = curNode.getSourceBlock() as BlockSvg; - const width = block.width; - const height = block.height; - const markerHeight = height * HEIGHT_MULTIPLIER; - const markerOffset = this.constants_.CURSOR_BLOCK_PADDING; - - if (block.previousConnection) { - const connectionShape = this.constants_.shapeFor( - block.previousConnection, - ) as Notch; - this.positionPrevious_( - width, - markerOffset, - markerHeight, - connectionShape, - ); - } else if (block.outputConnection) { - const connectionShape = this.constants_.shapeFor( - block.outputConnection, - ) as PuzzleTab; - this.positionOutput_(width, height, connectionShape); - } else { - this.positionBlock_(width, markerOffset, markerHeight); - } - this.setParent_(block); - this.showCurrent_(); - } - - /** - * Position and display the marker for a block. - * - * @param curNode The node to draw the marker for. - */ - protected showWithBlock_(curNode: ASTNode) { - this.showWithBlockPrevOutput(curNode); - } - - /** - * Position and display the marker for a previous connection. - * - * @param curNode The node to draw the marker for. - */ - protected showWithPrevious_(curNode: ASTNode) { - this.showWithBlockPrevOutput(curNode); - } - - /** - * Position and display the marker for an output connection. - * - * @param curNode The node to draw the marker for. - */ - protected showWithOutput_(curNode: ASTNode) { - this.showWithBlockPrevOutput(curNode); - } - - /** - * Position and display the marker for a workspace coordinate. - * This is a horizontal line. - * - * @param curNode The node to draw the marker for. - */ - protected showWithCoordinates_(curNode: ASTNode) { - const wsCoordinate = curNode.getWsCoordinate(); - let x = wsCoordinate?.x ?? 0; - const y = wsCoordinate?.y ?? 0; - - if (this.workspace.RTL) { - x -= this.constants_.CURSOR_WS_WIDTH; - } - - this.positionLine_(x, y, this.constants_.CURSOR_WS_WIDTH); - this.setParent_(this.workspace); - this.showCurrent_(); - } - - /** - * Position and display the marker for a field. - * This is a box around the field. - * - * @param curNode The node to draw the marker for. - */ - protected showWithField_(curNode: ASTNode) { - const field = curNode.getLocation() as Field; - const width = field.getSize().width; - const height = field.getSize().height; - - this.positionRect_(0, 0, width, height); - this.setParent_(field); - this.showCurrent_(); - } - - /** - * Position and display the marker for an input. - * This is a puzzle piece. - * - * @param curNode The node to draw the marker for. - */ - protected showWithInput_(curNode: ASTNode) { - const connection = curNode.getLocation() as RenderedConnection; - const sourceBlock = connection.getSourceBlock(); - - this.positionInput_(connection); - this.setParent_(sourceBlock); - this.showCurrent_(); - } - - /** - * Position and display the marker for a next connection. - * This is a horizontal line. - * - * @param curNode The node to draw the marker for. - */ - protected showWithNext_(curNode: ASTNode) { - const connection = curNode.getLocation() as RenderedConnection; - const targetBlock = connection.getSourceBlock(); - let x = 0; - const y = connection.getOffsetInBlock().y; - const width = targetBlock.getHeightWidth().width; - if (this.workspace.RTL) { - x = -width; - } - this.positionLine_(x, y, width); - this.setParent_(targetBlock); - this.showCurrent_(); - } - - /** - * Position and display the marker for a stack. - * This is a box with extra padding around the entire stack of blocks. - * - * @param curNode The node to draw the marker for. - */ - protected showWithStack_(curNode: ASTNode) { - const block = curNode.getLocation() as BlockSvg; - - // Gets the height and width of entire stack. - const heightWidth = block.getHeightWidth(); - - // Add padding so that being on a stack looks different than being on a - // block. - const width = heightWidth.width + this.constants_.CURSOR_STACK_PADDING; - const height = heightWidth.height + this.constants_.CURSOR_STACK_PADDING; - - // Shift the rectangle slightly to upper left so padding is equal on all - // sides. - const xPadding = -this.constants_.CURSOR_STACK_PADDING / 2; - const yPadding = -this.constants_.CURSOR_STACK_PADDING / 2; - - let x = xPadding; - const y = yPadding; - - if (this.workspace.RTL) { - x = -(width + xPadding); - } - this.positionRect_(x, y, width, height); - this.setParent_(block); - this.showCurrent_(); - } - - /** - * Position and display the marker for a flyout button. - * This is a box with extra padding around the button. - * - * @param curNode The node to draw the marker for. - */ - protected showWithButton_(curNode: ASTNode) { - const button = curNode.getLocation() as FlyoutButton; - - // Gets the height and width of entire stack. - const heightWidth = {height: button.height, width: button.width}; - - // Add padding so that being on a button looks similar to being on a stack. - const width = heightWidth.width + this.constants_.CURSOR_STACK_PADDING; - const height = heightWidth.height + this.constants_.CURSOR_STACK_PADDING; - - // Shift the rectangle slightly to upper left so padding is equal on all - // sides. - const xPadding = -this.constants_.CURSOR_STACK_PADDING / 2; - const yPadding = -this.constants_.CURSOR_STACK_PADDING / 2; - - let x = xPadding; - const y = yPadding; - - if (this.workspace.RTL) { - x = -(width + xPadding); - } - this.positionRect_(x, y, width, height); - this.setParent_(button); - this.showCurrent_(); - } - - /** Show the current marker. */ - protected showCurrent_() { - this.hide(); - if (this.currentMarkerSvg) { - this.currentMarkerSvg.style.display = ''; - } - } - - /************************** - * Position - **************************/ - - /** - * Position the marker for a block. - * Displays an outline of the top half of a rectangle around a block. - * - * @param width The width of the block. - * @param markerOffset The extra padding for around the block. - * @param markerHeight The height of the marker. - */ - protected positionBlock_( - width: number, - markerOffset: number, - markerHeight: number, - ) { - const markerPath = - svgPaths.moveBy(-markerOffset, markerHeight) + - svgPaths.lineOnAxis('V', -markerOffset) + - svgPaths.lineOnAxis('H', width + markerOffset * 2) + - svgPaths.lineOnAxis('V', markerHeight); - if (!this.markerBlock_) { - throw new Error( - 'createDom should be called before positioning the marker', - ); - } - this.markerBlock_.setAttribute('d', markerPath); - if (this.workspace.RTL) { - this.flipRtl(this.markerBlock_); - } - this.currentMarkerSvg = this.markerBlock_; - } - - /** - * Position the marker for an input connection. - * Displays a filled in puzzle piece. - * - * @param connection The connection to position marker around. - */ - protected positionInput_(connection: RenderedConnection) { - const x = connection.getOffsetInBlock().x; - const y = connection.getOffsetInBlock().y; - - const path = - svgPaths.moveTo(0, 0) + - (this.constants_.shapeFor(connection) as PuzzleTab).pathDown; - - this.markerInput_!.setAttribute('d', path); - this.markerInput_!.setAttribute( - 'transform', - 'translate(' + - x + - ',' + - y + - ')' + - (this.workspace.RTL ? ' scale(-1 1)' : ''), - ); - this.currentMarkerSvg = this.markerInput_; - } - - /** - * Move and show the marker at the specified coordinate in workspace units. - * Displays a horizontal line. - * - * @param x The new x, in workspace units. - * @param y The new y, in workspace units. - * @param width The new width, in workspace units. - */ - protected positionLine_(x: number, y: number, width: number) { - if (!this.markerSvgLine_) { - throw new Error('createDom should be called before positioning the line'); - } - this.markerSvgLine_.setAttribute('x', `${x}`); - this.markerSvgLine_.setAttribute('y', `${y}`); - this.markerSvgLine_.setAttribute('width', `${width}`); - this.currentMarkerSvg = this.markerSvgLine_; - } - - /** - * Position the marker for an output connection. - * Displays a puzzle outline and the top and bottom path. - * - * @param width The width of the block. - * @param height The height of the block. - * @param connectionShape The shape object for the connection. - */ - protected positionOutput_( - width: number, - height: number, - connectionShape: PuzzleTab, - ) { - if (!this.markerBlock_) { - throw new Error( - 'createDom should be called before positioning the output', - ); - } - const markerPath = - svgPaths.moveBy(width, 0) + - svgPaths.lineOnAxis('h', -(width - connectionShape.width)) + - svgPaths.lineOnAxis('v', this.constants_.TAB_OFFSET_FROM_TOP) + - connectionShape.pathDown + - svgPaths.lineOnAxis('V', height) + - svgPaths.lineOnAxis('H', width); - this.markerBlock_.setAttribute('d', markerPath); - if (this.workspace.RTL) { - this.flipRtl(this.markerBlock_); - } - this.currentMarkerSvg = this.markerBlock_; - } - - /** - * Position the marker for a previous connection. - * Displays a half rectangle with a notch in the top to represent the previous - * connection. - * - * @param width The width of the block. - * @param markerOffset The offset of the marker from around the block. - * @param markerHeight The height of the marker. - * @param connectionShape The shape object for the connection. - */ - protected positionPrevious_( - width: number, - markerOffset: number, - markerHeight: number, - connectionShape: Notch, - ) { - if (!this.markerBlock_) { - throw new Error( - 'createDom should be called before positioning the previous connection marker', - ); - } - const markerPath = - svgPaths.moveBy(-markerOffset, markerHeight) + - svgPaths.lineOnAxis('V', -markerOffset) + - svgPaths.lineOnAxis('H', this.constants_.NOTCH_OFFSET_LEFT) + - connectionShape.pathLeft + - svgPaths.lineOnAxis('H', width + markerOffset * 2) + - svgPaths.lineOnAxis('V', markerHeight); - this.markerBlock_.setAttribute('d', markerPath); - if (this.workspace.RTL) { - this.flipRtl(this.markerBlock_); - } - this.currentMarkerSvg = this.markerBlock_; - } - - /** - * Move and show the marker at the specified coordinate in workspace units. - * Displays a filled in rectangle. - * - * @param x The new x, in workspace units. - * @param y The new y, in workspace units. - * @param width The new width, in workspace units. - * @param height The new height, in workspace units. - */ - protected positionRect_(x: number, y: number, width: number, height: number) { - if (!this.markerSvgRect_) { - throw new Error('createDom should be called before positioning the rect'); - } - this.markerSvgRect_.setAttribute('x', `${x}`); - this.markerSvgRect_.setAttribute('y', `${y}`); - this.markerSvgRect_.setAttribute('width', `${width}`); - this.markerSvgRect_.setAttribute('height', `${height}`); - this.currentMarkerSvg = this.markerSvgRect_; - } - - /** - * Flip the SVG paths in RTL. - * - * @param markerSvg The marker that we want to flip. - */ - private flipRtl(markerSvg: SVGElement) { - markerSvg.setAttribute('transform', 'scale(-1 1)'); - } - - /** Hide the marker. */ - hide() { - if ( - !this.markerSvgLine_ || - !this.markerSvgRect_ || - !this.markerInput_ || - !this.markerBlock_ - ) { - throw new Error('createDom should be called before hiding the marker'); - } - this.markerSvgLine_.style.display = 'none'; - this.markerSvgRect_.style.display = 'none'; - this.markerInput_.style.display = 'none'; - this.markerBlock_.style.display = 'none'; - } - - /** - * Fire event for the marker or marker. - * - * @param oldNode The old node the marker used to be on. - * @param curNode The new node the marker is currently on. - */ - protected fireMarkerEvent(oldNode: ASTNode | null, curNode: ASTNode) { - const curBlock = curNode.getSourceBlock(); - const event = new (eventUtils.get(EventType.MARKER_MOVE))( - curBlock, - this.isCursor(), - oldNode, - curNode, - ); - eventUtils.fire(event); - } - - /** - * Get the properties to make a marker blink. - * - * @returns The object holding attributes to make the marker blink. - */ - protected getBlinkProperties_(): {[key: string]: string} { - return { - 'attributeType': 'XML', - 'attributeName': 'fill', - 'dur': '1s', - 'values': this.colour_ + ';transparent;transparent;', - 'repeatCount': 'indefinite', - }; - } - - /** - * Create the marker SVG. - * - * @returns The SVG node created. - */ - protected createDomInternal_(): Element { - /* This markup will be generated and added to the .svgGroup_: - - - - - - */ - - this.markerSvg_ = dom.createSvgElement( - Svg.G, - { - 'width': this.constants_.CURSOR_WS_WIDTH, - 'height': this.constants_.WS_CURSOR_HEIGHT, - }, - this.svgGroup_, - ); - - // A horizontal line used to represent a workspace coordinate or next - // connection. - this.markerSvgLine_ = dom.createSvgElement( - Svg.RECT, - { - 'width': this.constants_.CURSOR_WS_WIDTH, - 'height': this.constants_.WS_CURSOR_HEIGHT, - }, - this.markerSvg_, - ); - - // A filled in rectangle used to represent a stack. - this.markerSvgRect_ = dom.createSvgElement( - Svg.RECT, - { - 'class': 'blocklyVerticalMarker', - 'rx': 10, - 'ry': 10, - }, - this.markerSvg_, - ); - - // A filled in puzzle piece used to represent an input value. - this.markerInput_ = dom.createSvgElement( - Svg.PATH, - {'transform': ''}, - this.markerSvg_, - ); - - // A path used to represent a previous connection and a block, an output - // connection and a block, or a block. - this.markerBlock_ = dom.createSvgElement( - Svg.PATH, - { - 'transform': '', - 'fill': 'none', - 'stroke-width': this.constants_.CURSOR_STROKE_WIDTH, - }, - this.markerSvg_, - ); - - this.hide(); - - // Markers and stack markers don't blink. - if (this.isCursor()) { - const blinkProperties = this.getBlinkProperties_(); - dom.createSvgElement(Svg.ANIMATE, blinkProperties, this.markerSvgLine_); - dom.createSvgElement(Svg.ANIMATE, blinkProperties, this.markerInput_); - dom.createSvgElement( - Svg.ANIMATE, - {...blinkProperties, attributeName: 'stroke'}, - this.markerBlock_, - ); - } - - return this.markerSvg_; - } - - /** - * Apply the marker's colour. - * - * @param _curNode The node that we want to draw the marker for. - */ - protected applyColour_(_curNode: ASTNode) { - if ( - !this.markerSvgLine_ || - !this.markerSvgRect_ || - !this.markerInput_ || - !this.markerBlock_ - ) { - throw new Error( - 'createDom should be called before applying color to the markerj', - ); - } - this.markerSvgLine_.setAttribute('fill', this.colour_); - this.markerSvgRect_.setAttribute('stroke', this.colour_); - this.markerInput_.setAttribute('fill', this.colour_); - this.markerBlock_.setAttribute('stroke', this.colour_); - - if (this.isCursor()) { - const values = this.colour_ + ';transparent;transparent;'; - this.markerSvgLine_.firstElementChild!.setAttribute('values', values); - this.markerInput_.firstElementChild!.setAttribute('values', values); - this.markerBlock_.firstElementChild!.setAttribute('values', values); - } - } - - /** Dispose of this marker. */ - dispose() { - if (this.svgGroup_) { - dom.removeNode(this.svgGroup_); - } - } -} diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index ed2bb7dda75..7efc6318a31 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -24,18 +24,6 @@ export class PathObject implements IPathObject { svgRoot: SVGElement; svgPath: SVGElement; - /** - * Holds the cursors svg element when the cursor is attached to the block. - * This is null if there is no cursor on the block. - */ - cursorSvg: SVGElement | null = null; - - /** - * Holds the markers svg element when the marker is attached to the block. - * This is null if there is no marker on the block. - */ - markerSvg: SVGElement | null = null; - constants: ConstantProvider; style: BlockStyle; @@ -86,42 +74,6 @@ export class PathObject implements IPathObject { this.svgPath.setAttribute('transform', 'scale(-1 1)'); } - /** - * Add the cursor SVG to this block's SVG group. - * - * @param cursorSvg The SVG root of the cursor to be added to the block SVG - * group. - */ - setCursorSvg(cursorSvg: SVGElement) { - if (!cursorSvg) { - this.cursorSvg = null; - return; - } - - this.svgRoot.appendChild(cursorSvg); - this.cursorSvg = cursorSvg; - } - - /** - * Add the marker SVG to this block's SVG group. - * - * @param markerSvg The SVG root of the marker to be added to the block SVG - * group. - */ - setMarkerSvg(markerSvg: SVGElement) { - if (!markerSvg) { - this.markerSvg = null; - return; - } - - if (this.cursorSvg) { - this.svgRoot.insertBefore(markerSvg, this.cursorSvg); - } else { - this.svgRoot.appendChild(markerSvg); - } - this.markerSvg = markerSvg; - } - /** * Apply the stored colours to the block's path, taking into account whether * the paths belong to a shadow block. diff --git a/core/renderers/common/renderer.ts b/core/renderers/common/renderer.ts index 812ddd97678..5b7e687c25e 100644 --- a/core/renderers/common/renderer.ts +++ b/core/renderers/common/renderer.ts @@ -11,14 +11,11 @@ import type {BlockSvg} from '../../block_svg.js'; import {Connection} from '../../connection.js'; import {ConnectionType} from '../../connection_type.js'; import type {IRegistrable} from '../../interfaces/i_registrable.js'; -import type {Marker} from '../../keyboard_nav/marker.js'; import type {BlockStyle, Theme} from '../../theme.js'; -import type {WorkspaceSvg} from '../../workspace_svg.js'; import {ConstantProvider} from './constants.js'; import {Drawer} from './drawer.js'; import type {IPathObject} from './i_path_object.js'; import {RenderInfo} from './info.js'; -import {MarkerSvg} from './marker_svg.js'; import {PathObject} from './path_object.js'; /** @@ -167,17 +164,6 @@ export class Renderer implements IRegistrable { return new Drawer(block, info); } - /** - * Create a new instance of the renderer's marker drawer. - * - * @param workspace The workspace the marker belongs to. - * @param marker The marker. - * @returns The object in charge of drawing the marker. - */ - makeMarkerDrawer(workspace: WorkspaceSvg, marker: Marker): MarkerSvg { - return new MarkerSvg(workspace, this.getConstants(), marker); - } - /** * Create a new instance of a renderer path object. * diff --git a/core/renderers/zelos/marker_svg.ts b/core/renderers/zelos/marker_svg.ts deleted file mode 100644 index 395ece0b0be..00000000000 --- a/core/renderers/zelos/marker_svg.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -// Former goog.module ID: Blockly.zelos.MarkerSvg - -import type {BlockSvg} from '../../block_svg.js'; -import type {ASTNode} from '../../keyboard_nav/ast_node.js'; -import type {Marker} from '../../keyboard_nav/marker.js'; -import type {RenderedConnection} from '../../rendered_connection.js'; -import * as dom from '../../utils/dom.js'; -import {Svg} from '../../utils/svg.js'; -import type {WorkspaceSvg} from '../../workspace_svg.js'; -import type {ConstantProvider as BaseConstantProvider} from '../common/constants.js'; -import {MarkerSvg as BaseMarkerSvg} from '../common/marker_svg.js'; -import type {ConstantProvider as ZelosConstantProvider} from './constants.js'; - -/** - * Class to draw a marker. - */ -export class MarkerSvg extends BaseMarkerSvg { - // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. - constants_!: ZelosConstantProvider; - - private markerCircle: SVGCircleElement | null = null; - - /** - * @param workspace The workspace the marker belongs to. - * @param constants The constants for the renderer. - * @param marker The marker to draw. - */ - constructor( - workspace: WorkspaceSvg, - constants: BaseConstantProvider, - marker: Marker, - ) { - super(workspace, constants, marker); - } - - /** - * Position and display the marker for an input or an output connection. - * - * @param curNode The node to draw the marker for. - */ - private showWithInputOutput(curNode: ASTNode) { - const block = curNode.getSourceBlock() as BlockSvg; - const connection = curNode.getLocation() as RenderedConnection; - const offsetInBlock = connection.getOffsetInBlock(); - - this.positionCircle(offsetInBlock.x, offsetInBlock.y); - this.setParent_(block); - this.showCurrent_(); - } - - override showWithOutput_(curNode: ASTNode) { - this.showWithInputOutput(curNode); - } - - override showWithInput_(curNode: ASTNode) { - this.showWithInputOutput(curNode); - } - - /** - * Draw a rectangle around the block. - * - * @param curNode The current node of the marker. - */ - override showWithBlock_(curNode: ASTNode) { - const block = curNode.getLocation() as BlockSvg; - - // Gets the height and width of entire stack. - const heightWidth = block.getHeightWidth(); - // Add padding so that being on a stack looks different than being on a - // block. - this.positionRect_(0, 0, heightWidth.width, heightWidth.height); - this.setParent_(block); - this.showCurrent_(); - } - - /** - * Position the circle we use for input and output connections. - * - * @param x The x position of the circle. - * @param y The y position of the circle. - */ - private positionCircle(x: number, y: number) { - this.markerCircle?.setAttribute('cx', `${x}`); - this.markerCircle?.setAttribute('cy', `${y}`); - this.currentMarkerSvg = this.markerCircle; - } - - override hide() { - super.hide(); - if (this.markerCircle) { - this.markerCircle.style.display = 'none'; - } - } - - override createDomInternal_() { - /* clang-format off */ - /* This markup will be generated and added to the .svgGroup_: - - - - - - */ - /* clang-format on */ - super.createDomInternal_(); - - this.markerCircle = dom.createSvgElement( - Svg.CIRCLE, - { - 'r': this.constants_.CURSOR_RADIUS, - 'stroke-width': this.constants_.CURSOR_STROKE_WIDTH, - }, - this.markerSvg_, - ); - this.hide(); - - // Markers and stack cursors don't blink. - if (this.isCursor()) { - const blinkProperties = this.getBlinkProperties_(); - dom.createSvgElement(Svg.ANIMATE, blinkProperties, this.markerCircle!); - } - - return this.markerSvg_!; - } - - override applyColour_(curNode: ASTNode) { - super.applyColour_(curNode); - - this.markerCircle?.setAttribute('fill', this.colour_); - this.markerCircle?.setAttribute('stroke', this.colour_); - - if (this.isCursor()) { - const values = this.colour_ + ';transparent;transparent;'; - this.markerCircle?.firstElementChild!.setAttribute('values', values); - } - } -} diff --git a/core/renderers/zelos/renderer.ts b/core/renderers/zelos/renderer.ts index c880ce9f80b..367d96faf51 100644 --- a/core/renderers/zelos/renderer.ts +++ b/core/renderers/zelos/renderer.ts @@ -7,16 +7,13 @@ // Former goog.module ID: Blockly.zelos.Renderer import type {BlockSvg} from '../../block_svg.js'; -import type {Marker} from '../../keyboard_nav/marker.js'; import type {BlockStyle} from '../../theme.js'; -import type {WorkspaceSvg} from '../../workspace_svg.js'; import * as blockRendering from '../common/block_rendering.js'; import type {RenderInfo as BaseRenderInfo} from '../common/info.js'; import {Renderer as BaseRenderer} from '../common/renderer.js'; import {ConstantProvider} from './constants.js'; import {Drawer} from './drawer.js'; import {RenderInfo} from './info.js'; -import {MarkerSvg} from './marker_svg.js'; import {PathObject} from './path_object.js'; /** @@ -69,20 +66,6 @@ export class Renderer extends BaseRenderer { return new Drawer(block, info as RenderInfo); } - /** - * Create a new instance of the renderer's cursor drawer. - * - * @param workspace The workspace the cursor belongs to. - * @param marker The marker. - * @returns The object in charge of drawing the marker. - */ - override makeMarkerDrawer( - workspace: WorkspaceSvg, - marker: Marker, - ): MarkerSvg { - return new MarkerSvg(workspace, this.getConstants(), marker); - } - /** * Create a new instance of a renderer path object. * diff --git a/core/renderers/zelos/zelos.ts b/core/renderers/zelos/zelos.ts index c28a0210c6d..5b0a7c51c60 100644 --- a/core/renderers/zelos/zelos.ts +++ b/core/renderers/zelos/zelos.ts @@ -11,7 +11,6 @@ import {ConstantProvider} from './constants.js'; import {Drawer} from './drawer.js'; import {RenderInfo} from './info.js'; -import {MarkerSvg} from './marker_svg.js'; import {BottomRow} from './measurables/bottom_row.js'; import {StatementInput} from './measurables/inputs.js'; import {RightConnectionShape} from './measurables/row_elements.js'; @@ -23,7 +22,6 @@ export { BottomRow, ConstantProvider, Drawer, - MarkerSvg, PathObject, Renderer, RenderInfo, diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index a8930fbb44e..3ff73896ad1 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -64,7 +64,6 @@ import {Navigator} from './navigator.js'; import {Options} from './options.js'; import * as Procedures from './procedures.js'; import * as registry from './registry.js'; -import * as renderManagement from './render_management.js'; import * as blockRendering from './renderers/common/block_rendering.js'; import type {Renderer} from './renderers/common/renderer.js'; import type {ScrollbarPair} from './scrollbar_pair.js'; @@ -474,28 +473,6 @@ export class WorkspaceSvg return this.componentManager; } - /** - * Add the cursor SVG to this workspaces SVG group. - * - * @param cursorSvg The SVG root of the cursor to be added to the workspace - * SVG group. - * @internal - */ - setCursorSvg(cursorSvg: SVGElement) { - this.markerManager.setCursorSvg(cursorSvg); - } - - /** - * Add the marker SVG to this workspaces SVG group. - * - * @param markerSvg The SVG root of the marker to be added to the workspace - * SVG group. - * @internal - */ - setMarkerSvg(markerSvg: SVGElement) { - this.markerManager.setMarkerSvg(markerSvg); - } - /** * Get the marker with the given ID. * @@ -1340,10 +1317,6 @@ export class WorkspaceSvg .flatMap((block) => block.getDescendants(false)) .filter((block) => block.isInsertionMarker()) .forEach((block) => block.queueRender()); - - renderManagement - .finishQueuedRenders() - .then(() => void this.markerManager.updateMarkers()); } /** From e5e32548d6f8e46cfb2d71bd5f2a48dec12e27c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 May 2025 13:49:51 -0700 Subject: [PATCH 191/222] chore(deps): bump @microsoft/api-documenter from 7.26.7 to 7.26.26 (#8987) * chore(deps): bump @microsoft/api-documenter from 7.26.7 to 7.26.26 Bumps [@microsoft/api-documenter](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-documenter) from 7.26.7 to 7.26.26. - [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-documenter/CHANGELOG.md) - [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-documenter_v7.26.26/apps/api-documenter) --- updated-dependencies: - dependency-name: "@microsoft/api-documenter" dependency-version: 7.26.26 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * fix(deps): Update patch for api-documenter The patch was failing to apply because the contents of the file had moved too much. Fixed by manually applying changes to the file in question then running node_modules/.bin/patch-package @microsoft/api-documenter to recreate the patch, then diffing old patch vs. new to verify that contents of patch were unchanged except for chunk positions, then deleting old patch. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Christopher Allen --- package-lock.json | 44 +++++++++---------- ...> @microsoft+api-documenter+7.26.26.patch} | 10 ++--- 2 files changed, 27 insertions(+), 27 deletions(-) rename patches/{@microsoft+api-documenter+7.22.4.patch => @microsoft+api-documenter+7.26.26.patch} (94%) diff --git a/package-lock.json b/package-lock.json index 6cc798fb482..294d8149d4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -857,17 +857,17 @@ } }, "node_modules/@microsoft/api-documenter": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.26.7.tgz", - "integrity": "sha512-VruYlHYAhQfuBNyndvyD9GBmCRWSOZ8D9+eXicygycNPMC/SPM71WWu2OwP98CTBvq/OhG/uRvUGGdL4QvgiFQ==", + "version": "7.26.26", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.26.26.tgz", + "integrity": "sha512-085FwdwQcXGvwtMJFajwhu5eZOQ3PXsyLIoq3WXAQr/7M6Vn59GMGjuB/+lIXqmWKkxzeFAX5f9sKqr9X7zI3g==", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/api-extractor-model": "7.30.3", + "@microsoft/api-extractor-model": "7.30.6", "@microsoft/tsdoc": "~0.15.1", - "@rushstack/node-core-library": "5.11.0", - "@rushstack/terminal": "0.14.6", - "@rushstack/ts-command-line": "4.23.4", + "@rushstack/node-core-library": "5.13.1", + "@rushstack/terminal": "0.15.3", + "@rushstack/ts-command-line": "5.0.1", "js-yaml": "~3.13.1", "resolve": "~1.22.1" }, @@ -922,15 +922,15 @@ } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.30.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.3.tgz", - "integrity": "sha512-yEAvq0F78MmStXdqz9TTT4PZ05Xu5R8nqgwI5xmUmQjWBQ9E6R2n8HB/iZMRciG4rf9iwI2mtuQwIzDXBvHn1w==", + "version": "7.30.6", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.6.tgz", + "integrity": "sha512-znmFn69wf/AIrwHya3fxX6uB5etSIn6vg4Q4RB/tb5VDDs1rqREc+AvMC/p19MUN13CZ7+V/8pkYPTj7q8tftg==", "dev": true, "license": "MIT", "dependencies": { "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.11.0" + "@rushstack/node-core-library": "5.13.1" } }, "node_modules/@microsoft/api-extractor/node_modules/@microsoft/api-extractor-model": { @@ -1257,9 +1257,9 @@ } }, "node_modules/@rushstack/node-core-library": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.11.0.tgz", - "integrity": "sha512-I8+VzG9A0F3nH2rLpPd7hF8F7l5Xb7D+ldrWVZYegXM6CsKkvWc670RlgK3WX8/AseZfXA/vVrh0bpXe2Y2UDQ==", + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.13.1.tgz", + "integrity": "sha512-5yXhzPFGEkVc9Fu92wsNJ9jlvdwz4RNb2bMso+/+TH0nMm1jDDDsOIf4l8GAkPxGuwPw5DH24RliWVfSPhlW/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1331,13 +1331,13 @@ } }, "node_modules/@rushstack/terminal": { - "version": "0.14.6", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.14.6.tgz", - "integrity": "sha512-4nMUy4h0u5PGXVG71kEA9uYI3l8GjVqewoHOFONiM6fuqS51ORdaJZ5ZXB2VZEGUyfg1TOTSy88MF2cdAy+lqA==", + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.3.tgz", + "integrity": "sha512-DGJ0B2Vm69468kZCJkPj3AH5nN+nR9SPmC0rFHtzsS4lBQ7/dgOwtwVxYP7W9JPDMuRBkJ4KHmWKr036eJsj9g==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.11.0", + "@rushstack/node-core-library": "5.13.1", "supports-color": "~8.1.1" }, "peerDependencies": { @@ -1366,13 +1366,13 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "4.23.4", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.4.tgz", - "integrity": "sha512-pqmzDJCm0TS8VyeqnzcJ7ncwXgiLDQ6LVmXXfqv2nPL6VIz+UpyTpNVfZRJpyyJ+UDxqob1vIj2liaUfBjv8/A==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.0.1.tgz", + "integrity": "sha512-bsbUucn41UXrQK7wgM8CNM/jagBytEyJqXw/umtI8d68vFm1Jwxh1OtLrlW7uGZgjCWiiPH6ooUNa1aVsuVr3Q==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.14.6", + "@rushstack/terminal": "0.15.3", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" diff --git a/patches/@microsoft+api-documenter+7.22.4.patch b/patches/@microsoft+api-documenter+7.26.26.patch similarity index 94% rename from patches/@microsoft+api-documenter+7.22.4.patch rename to patches/@microsoft+api-documenter+7.26.26.patch index 6e039a391e8..de8e47c0419 100644 --- a/patches/@microsoft+api-documenter+7.22.4.patch +++ b/patches/@microsoft+api-documenter+7.26.26.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js b/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js -index 5284d10..f2f9d14 100644 +index 0f4e2ba..3af2014 100644 --- a/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js +++ b/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js -@@ -877,12 +877,15 @@ class MarkdownDocumenter { +@@ -893,12 +893,15 @@ class MarkdownDocumenter { } _writeBreadcrumb(output, apiItem) { const configuration = this._tsdocConfiguration; @@ -24,7 +24,7 @@ index 5284d10..f2f9d14 100644 for (const hierarchyItem of apiItem.getHierarchy()) { switch (hierarchyItem.kind) { case api_extractor_model_1.ApiItemKind.Model: -@@ -892,18 +895,24 @@ class MarkdownDocumenter { +@@ -908,18 +911,24 @@ class MarkdownDocumenter { // this may change in the future. break; default: @@ -55,7 +55,7 @@ index 5284d10..f2f9d14 100644 } } } -@@ -968,11 +977,8 @@ class MarkdownDocumenter { +@@ -992,11 +1001,8 @@ class MarkdownDocumenter { // For overloaded methods, add a suffix such as "MyClass.myMethod_2". let qualifiedName = Utilities_1.Utilities.getSafeFilenameForName(hierarchyItem.displayName); if (api_extractor_model_1.ApiParameterListMixin.isBaseClassOf(hierarchyItem)) { @@ -69,7 +69,7 @@ index 5284d10..f2f9d14 100644 } switch (hierarchyItem.kind) { case api_extractor_model_1.ApiItemKind.Model: -@@ -983,7 +989,8 @@ class MarkdownDocumenter { +@@ -1007,7 +1013,8 @@ class MarkdownDocumenter { baseName = Utilities_1.Utilities.getSafeFilenameForName(node_core_library_1.PackageName.getUnscopedName(hierarchyItem.displayName)); break; default: From 8edd3732b8cac20435fb8e490ebe6b9041264401 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Wed, 7 May 2025 17:14:20 -0700 Subject: [PATCH 192/222] chore: bump version to 12.0.0-beta.5 (#9012) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 77f2f8728ed..02de252439d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "12.0.0-beta.4", + "version": "12.0.0-beta.5", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly" From b6b229eb443e3ac5b548584e1d316b239b98aaab Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 8 May 2025 10:52:43 -0700 Subject: [PATCH 193/222] fix!: delete marker move event and tests (#9013) --- core/events/events.ts | 2 - core/events/events_marker_move.ts | 133 -------------------------- core/events/predicates.ts | 6 -- tests/mocha/event_marker_move_test.js | 44 --------- tests/mocha/event_test.js | 80 ---------------- tests/mocha/index.html | 1 - 6 files changed, 266 deletions(-) delete mode 100644 core/events/events_marker_move.ts delete mode 100644 tests/mocha/event_marker_move_test.js diff --git a/core/events/events.ts b/core/events/events.ts index ae3b9e6b21f..dcddf19a925 100644 --- a/core/events/events.ts +++ b/core/events/events.ts @@ -33,7 +33,6 @@ export {CommentDelete} from './events_comment_delete.js'; export {CommentDrag, CommentDragJson} from './events_comment_drag.js'; export {CommentMove, CommentMoveJson} from './events_comment_move.js'; export {CommentResize, CommentResizeJson} from './events_comment_resize.js'; -export {MarkerMove, MarkerMoveJson} from './events_marker_move.js'; export {Selected, SelectedJson} from './events_selected.js'; export {ThemeChange, ThemeChangeJson} from './events_theme_change.js'; export { @@ -77,7 +76,6 @@ export const CREATE = EventType.BLOCK_CREATE; /** @deprecated Use BLOCK_DELETE instead */ export const DELETE = EventType.BLOCK_DELETE; export const FINISHED_LOADING = EventType.FINISHED_LOADING; -export const MARKER_MOVE = EventType.MARKER_MOVE; /** @deprecated Use BLOCK_MOVE instead */ export const MOVE = EventType.BLOCK_MOVE; export const SELECTED = EventType.SELECTED; diff --git a/core/events/events_marker_move.ts b/core/events/events_marker_move.ts deleted file mode 100644 index 58309df5896..00000000000 --- a/core/events/events_marker_move.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Events fired as a result of a marker move. - * - * @class - */ -// Former goog.module ID: Blockly.Events.MarkerMove - -import type {Block} from '../block.js'; -import {ASTNode} from '../keyboard_nav/ast_node.js'; -import * as registry from '../registry.js'; -import type {Workspace} from '../workspace.js'; -import {AbstractEventJson} from './events_abstract.js'; -import {UiBase} from './events_ui_base.js'; -import {EventType} from './type.js'; - -/** - * Notifies listeners that a marker (used for keyboard navigation) has - * moved. - */ -export class MarkerMove extends UiBase { - /** The ID of the block the marker is now on, if any. */ - blockId?: string; - - /** The old node the marker used to be on, if any. */ - oldNode?: ASTNode; - - /** The new node the marker is now on. */ - newNode?: ASTNode; - - /** - * True if this is a cursor event, false otherwise. - * For information about cursors vs markers see {@link - * https://blocklycodelabs.dev/codelabs/keyboard-navigation/index.html?index=..%2F..index#1}. - */ - isCursor?: boolean; - - override type = EventType.MARKER_MOVE; - - /** - * @param opt_block The affected block. Null if current node is of type - * workspace. Undefined for a blank event. - * @param isCursor Whether this is a cursor event. Undefined for a blank - * event. - * @param opt_oldNode The old node the marker used to be on. - * Undefined for a blank event. - * @param opt_newNode The new node the marker is now on. - * Undefined for a blank event. - */ - constructor( - opt_block?: Block | null, - isCursor?: boolean, - opt_oldNode?: ASTNode | null, - opt_newNode?: ASTNode, - ) { - let workspaceId = opt_block ? opt_block.workspace.id : undefined; - if (opt_newNode && opt_newNode.getType() === ASTNode.types.WORKSPACE) { - workspaceId = (opt_newNode.getLocation() as Workspace).id; - } - super(workspaceId); - - this.blockId = opt_block?.id; - this.oldNode = opt_oldNode || undefined; - this.newNode = opt_newNode; - this.isCursor = isCursor; - } - - /** - * Encode the event as JSON. - * - * @returns JSON representation. - */ - override toJson(): MarkerMoveJson { - const json = super.toJson() as MarkerMoveJson; - if (this.isCursor === undefined) { - throw new Error( - 'Whether this is a cursor event or not is undefined. Either pass ' + - 'a value to the constructor, or call fromJson', - ); - } - if (!this.newNode) { - throw new Error( - 'The new node is undefined. Either pass a node to ' + - 'the constructor, or call fromJson', - ); - } - json['isCursor'] = this.isCursor; - json['blockId'] = this.blockId; - json['oldNode'] = this.oldNode; - json['newNode'] = this.newNode; - return json; - } - - /** - * Deserializes the JSON event. - * - * @param event The event to append new properties to. Should be a subclass - * of MarkerMove, but we can't specify that due to the fact that - * parameters to static methods in subclasses must be supertypes of - * parameters to static methods in superclasses. - * @internal - */ - static fromJson( - json: MarkerMoveJson, - workspace: Workspace, - event?: any, - ): MarkerMove { - const newEvent = super.fromJson( - json, - workspace, - event ?? new MarkerMove(), - ) as MarkerMove; - newEvent.isCursor = json['isCursor']; - newEvent.blockId = json['blockId']; - newEvent.oldNode = json['oldNode']; - newEvent.newNode = json['newNode']; - return newEvent; - } -} - -export interface MarkerMoveJson extends AbstractEventJson { - isCursor: boolean; - blockId?: string; - oldNode?: ASTNode; - newNode: ASTNode; -} - -registry.register(registry.Type.EVENT, EventType.MARKER_MOVE, MarkerMove); diff --git a/core/events/predicates.ts b/core/events/predicates.ts index 79d8ca284e4..9e8ce3b3a59 100644 --- a/core/events/predicates.ts +++ b/core/events/predicates.ts @@ -29,7 +29,6 @@ import type {CommentDelete} from './events_comment_delete.js'; import type {CommentDrag} from './events_comment_drag.js'; import type {CommentMove} from './events_comment_move.js'; import type {CommentResize} from './events_comment_resize.js'; -import type {MarkerMove} from './events_marker_move.js'; import type {Selected} from './events_selected.js'; import type {ThemeChange} from './events_theme_change.js'; import type {ToolboxItemSelect} from './events_toolbox_item_select.js'; @@ -99,11 +98,6 @@ export function isClick(event: Abstract): event is Click { return event.type === EventType.CLICK; } -/** @returns true iff event.type is EventType.MARKER_MOVE */ -export function isMarkerMove(event: Abstract): event is MarkerMove { - return event.type === EventType.MARKER_MOVE; -} - /** @returns true iff event.type is EventType.BUBBLE_OPEN */ export function isBubbleOpen(event: Abstract): event is BubbleOpen { return event.type === EventType.BUBBLE_OPEN; diff --git a/tests/mocha/event_marker_move_test.js b/tests/mocha/event_marker_move_test.js deleted file mode 100644 index cd5609c33d7..00000000000 --- a/tests/mocha/event_marker_move_test.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @license - * Copyright 2022 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import {assert} from '../../node_modules/chai/chai.js'; -import {defineRowBlock} from './test_helpers/block_definitions.js'; -import { - sharedTestSetup, - sharedTestTeardown, -} from './test_helpers/setup_teardown.js'; - -suite('Marker Move Event', function () { - setup(function () { - sharedTestSetup.call(this); - defineRowBlock(); - this.workspace = new Blockly.Workspace(); - }); - - teardown(function () { - sharedTestTeardown.call(this); - }); - - suite('Serialization', function () { - test('events round-trip through JSON', function () { - const block1 = this.workspace.newBlock('row_block', 'test_id1'); - const block2 = this.workspace.newBlock('row_block', 'test_id2'); - const node1 = new Blockly.ASTNode(Blockly.ASTNode.types.BLOCK, block1); - const node2 = new Blockly.ASTNode(Blockly.ASTNode.types.BLOCK, block2); - const origEvent = new Blockly.Events.MarkerMove( - block2, - false, - node1, - node2, - ); - - const json = origEvent.toJson(); - const newEvent = new Blockly.Events.fromJson(json, this.workspace); - - assert.deepEqual(newEvent, origEvent); - }); - }); -}); diff --git a/tests/mocha/event_test.js b/tests/mocha/event_test.js index 84ea7f0d78e..ea58983c676 100644 --- a/tests/mocha/event_test.js +++ b/tests/mocha/event_test.js @@ -6,7 +6,6 @@ import * as Blockly from '../../build/src/core/blockly.js'; import * as eventUtils from '../../build/src/core/events/utils.js'; -import {ASTNode} from '../../build/src/core/keyboard_nav/ast_node.js'; import {assert} from '../../node_modules/chai/chai.js'; import { assertEventEquals, @@ -519,85 +518,6 @@ suite('Events', function () { newValue: 'new value', }), }, - { - title: 'null to Block Marker move', - class: Blockly.Events.MarkerMove, - getArgs: (thisObj) => [ - thisObj.block, - true, - null, - new ASTNode(ASTNode.types.BLOCK, thisObj.block), - ], - getExpectedJson: (thisObj) => ({ - type: 'marker_move', - group: '', - isCursor: true, - blockId: thisObj.block.id, - oldNode: undefined, - newNode: new ASTNode(ASTNode.types.BLOCK, thisObj.block), - }), - }, - { - title: 'null to Workspace Marker move', - class: Blockly.Events.MarkerMove, - getArgs: (thisObj) => [ - null, - true, - null, - ASTNode.createWorkspaceNode( - thisObj.workspace, - new Blockly.utils.Coordinate(0, 0), - ), - ], - getExpectedJson: (thisObj) => ({ - type: 'marker_move', - group: '', - isCursor: true, - blockId: undefined, - oldNode: undefined, - newNode: ASTNode.createWorkspaceNode( - thisObj.workspace, - new Blockly.utils.Coordinate(0, 0), - ), - }), - }, - { - title: 'Workspace to Block Marker move', - class: Blockly.Events.MarkerMove, - getArgs: (thisObj) => [ - thisObj.block, - true, - ASTNode.createWorkspaceNode( - thisObj.workspace, - new Blockly.utils.Coordinate(0, 0), - ), - new ASTNode(ASTNode.types.BLOCK, thisObj.block), - ], - getExpectedJson: (thisObj) => ({ - type: 'marker_move', - group: '', - isCursor: true, - blockId: thisObj.block.id, - oldNode: ASTNode.createWorkspaceNode( - thisObj.workspace, - new Blockly.utils.Coordinate(0, 0), - ), - newNode: new ASTNode(ASTNode.types.BLOCK, thisObj.block), - }), - }, - { - title: 'Block to Workspace Marker move', - class: Blockly.Events.MarkerMove, - getArgs: (thisObj) => [ - null, - true, - new ASTNode(ASTNode.types.BLOCK, thisObj.block), - ASTNode.createWorkspaceNode( - thisObj.workspace, - new Blockly.utils.Coordinate(0, 0), - ), - ], - }, { title: 'Selected', class: Blockly.Events.Selected, diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 23216e10c68..09ef8820f0e 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -209,7 +209,6 @@ import './event_comment_move_test.js'; import './event_comment_drag_test.js'; import './event_comment_resize_test.js'; - import './event_marker_move_test.js'; import './event_selected_test.js'; import './event_theme_change_test.js'; import './event_toolbox_item_select_test.js'; From 561129ac612662e8b77a12556265eae348aac054 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 8 May 2025 11:47:42 -0700 Subject: [PATCH 194/222] fix!: delete ASTNode and references (#9014) --- core/blockly.ts | 2 - core/keyboard_nav/ast_node.ts | 336 ------------------------------- core/keyboard_nav/line_cursor.ts | 2 +- core/keyboard_nav/marker.ts | 33 +-- 4 files changed, 8 insertions(+), 365 deletions(-) delete mode 100644 core/keyboard_nav/ast_node.ts diff --git a/core/blockly.ts b/core/blockly.ts index 682649673c6..f250562a4d8 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -174,7 +174,6 @@ import { import {IVariableMap} from './interfaces/i_variable_map.js'; import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import * as internalConstants from './internal_constants.js'; -import {ASTNode} from './keyboard_nav/ast_node.js'; import {CursorOptions, LineCursor} from './keyboard_nav/line_cursor.js'; import {Marker} from './keyboard_nav/marker.js'; import type {LayerManager} from './layer_manager.js'; @@ -447,7 +446,6 @@ export * from './toast.js'; // Re-export submodules that no longer declareLegacyNamespace. export { - ASTNode, Block, BlockSvg, BlocklyOptions, diff --git a/core/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts deleted file mode 100644 index ea58fd0ab5f..00000000000 --- a/core/keyboard_nav/ast_node.ts +++ /dev/null @@ -1,336 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * The class representing an AST node. - * Used to traverse the Blockly AST. - * - * @class - */ -// Former goog.module ID: Blockly.ASTNode - -import {Block} from '../block.js'; -import type {Connection} from '../connection.js'; -import {ConnectionType} from '../connection_type.js'; -import type {Field} from '../field.js'; -import {FlyoutButton} from '../flyout_button.js'; -import type {Input} from '../inputs/input.js'; -import type {IASTNodeLocation} from '../interfaces/i_ast_node_location.js'; -import type {IASTNodeLocationWithBlock} from '../interfaces/i_ast_node_location_with_block.js'; -import {Coordinate} from '../utils/coordinate.js'; -import type {Workspace} from '../workspace.js'; - -/** - * Class for an AST node. - * It is recommended that you use one of the createNode methods instead of - * creating a node directly. - */ -export class ASTNode { - /** - * True to navigate to all fields. False to only navigate to clickable fields. - */ - static NAVIGATE_ALL_FIELDS = false; - - /** - * The default y offset to use when moving the cursor from a stack to the - * workspace. - */ - private static readonly DEFAULT_OFFSET_Y: number = -20; - private readonly type: string; - private readonly isConnectionLocation: boolean; - private readonly location: IASTNodeLocation; - - /** The coordinate on the workspace. */ - private wsCoordinate: Coordinate | null = null; - - /** - * @param type The type of the location. - * Must be in ASTNode.types. - * @param location The position in the AST. - * @param opt_params Optional dictionary of options. - */ - constructor(type: string, location: IASTNodeLocation, opt_params?: Params) { - if (!location) { - throw Error('Cannot create a node without a location.'); - } - - /** - * The type of the location. - * One of ASTNode.types - */ - this.type = type; - - /** Whether the location points to a connection. */ - this.isConnectionLocation = ASTNode.isConnectionType(type); - - /** The location of the AST node. */ - this.location = location; - - this.processParams(opt_params || null); - } - - /** - * Parse the optional parameters. - * - * @param params The user specified parameters. - */ - private processParams(params: Params | null) { - if (!params) { - return; - } - if (params.wsCoordinate) { - this.wsCoordinate = params.wsCoordinate; - } - } - - /** - * Gets the value pointed to by this node. - * It is the callers responsibility to check the node type to figure out what - * type of object they get back from this. - * - * @returns The current field, connection, workspace, or block the cursor is - * on. - */ - getLocation(): IASTNodeLocation { - return this.location; - } - - /** - * The type of the current location. - * One of ASTNode.types - * - * @returns The type of the location. - */ - getType(): string { - return this.type; - } - - /** - * The coordinate on the workspace. - * - * @returns The workspace coordinate or null if the location is not a - * workspace. - */ - getWsCoordinate(): Coordinate | null { - return this.wsCoordinate; - } - - /** - * Whether the node points to a connection. - * - * @returns [description] - * @internal - */ - isConnection(): boolean { - return this.isConnectionLocation; - } - - private getVisibleInputs(block: Block): Input[] { - return block.inputList.filter((input) => input.isVisible()); - } - - /** - * Finds the source block of the location of this node. - * - * @returns The source block of the location, or null if the node is of type - * workspace or button. - */ - getSourceBlock(): Block | null { - if (this.getType() === ASTNode.types.BLOCK) { - return this.getLocation() as Block; - } else if (this.getType() === ASTNode.types.STACK) { - return this.getLocation() as Block; - } else if (this.getType() === ASTNode.types.WORKSPACE) { - return null; - } else if (this.getType() === ASTNode.types.BUTTON) { - return null; - } else { - return (this.getLocation() as IASTNodeLocationWithBlock).getSourceBlock(); - } - } - - /** - * Whether an AST node of the given type points to a connection. - * - * @param type The type to check. One of ASTNode.types. - * @returns True if a node of the given type points to a connection. - */ - private static isConnectionType(type: string): boolean { - switch (type) { - case ASTNode.types.PREVIOUS: - case ASTNode.types.NEXT: - case ASTNode.types.INPUT: - case ASTNode.types.OUTPUT: - return true; - } - return false; - } - - /** - * Create an AST node pointing to a field. - * - * @param field The location of the AST node. - * @returns An AST node pointing to a field. - */ - static createFieldNode(field: Field): ASTNode { - return new ASTNode(ASTNode.types.FIELD, field); - } - - /** - * Creates an AST node pointing to a connection. If the connection has a - * parent input then create an AST node of type input that will hold the - * connection. - * - * @param connection This is the connection the node will point to. - * @returns An AST node pointing to a connection. - */ - static createConnectionNode(connection: Connection): ASTNode | null { - const type = connection.type; - const parentInput = connection.getParentInput(); - if ( - (type === ConnectionType.INPUT_VALUE || - type === ConnectionType.NEXT_STATEMENT) && - parentInput - ) { - return ASTNode.createInputNode(parentInput); - } else if (type === ConnectionType.NEXT_STATEMENT) { - return new ASTNode(ASTNode.types.NEXT, connection); - } else if (type === ConnectionType.OUTPUT_VALUE) { - return new ASTNode(ASTNode.types.OUTPUT, connection); - } else if (type === ConnectionType.PREVIOUS_STATEMENT) { - return new ASTNode(ASTNode.types.PREVIOUS, connection); - } - return null; - } - - /** - * Creates an AST node pointing to an input. Stores the input connection as - * the location. - * - * @param input The input used to create an AST node. - * @returns An AST node pointing to a input. - */ - static createInputNode(input: Input): ASTNode | null { - if (!input.connection) { - return null; - } - return new ASTNode(ASTNode.types.INPUT, input.connection); - } - - /** - * Creates an AST node pointing to a block. - * - * @param block The block used to create an AST node. - * @returns An AST node pointing to a block. - */ - static createBlockNode(block: Block): ASTNode { - return new ASTNode(ASTNode.types.BLOCK, block); - } - - /** - * Create an AST node of type stack. A stack, represented by its top block, is - * the set of all blocks connected to a top block, including the top - * block. - * - * @param topBlock A top block has no parent and can be found in the list - * returned by workspace.getTopBlocks(). - * @returns An AST node of type stack that points to the top block on the - * stack. - */ - static createStackNode(topBlock: Block): ASTNode { - return new ASTNode(ASTNode.types.STACK, topBlock); - } - - /** - * Create an AST node of type button. A button in this case refers - * specifically to a button in a flyout. - * - * @param button A top block has no parent and can be found in the list - * returned by workspace.getTopBlocks(). - * @returns An AST node of type stack that points to the top block on the - * stack. - */ - static createButtonNode(button: FlyoutButton): ASTNode { - return new ASTNode(ASTNode.types.BUTTON, button); - } - - /** - * Creates an AST node pointing to a workspace. - * - * @param workspace The workspace that we are on. - * @param wsCoordinate The position on the workspace for this node. - * @returns An AST node pointing to a workspace and a position on the - * workspace. - */ - static createWorkspaceNode( - workspace: Workspace, - wsCoordinate: Coordinate, - ): ASTNode { - const params = {wsCoordinate}; - return new ASTNode(ASTNode.types.WORKSPACE, workspace, params); - } - - /** - * Creates an AST node for the top position on a block. - * This is either an output connection, previous connection, or block. - * - * @param block The block to find the top most AST node on. - * @returns The AST node holding the top most position on the block. - */ - static createTopNode(block: Block): ASTNode | null { - let astNode; - const topConnection = getParentConnection(block); - if (topConnection) { - astNode = ASTNode.createConnectionNode(topConnection); - } else { - astNode = ASTNode.createBlockNode(block); - } - return astNode; - } -} - -export namespace ASTNode { - export interface Params { - wsCoordinate: Coordinate; - } - - export enum types { - FIELD = 'field', - BLOCK = 'block', - INPUT = 'input', - OUTPUT = 'output', - NEXT = 'next', - PREVIOUS = 'previous', - STACK = 'stack', - WORKSPACE = 'workspace', - BUTTON = 'button', - } -} - -export type Params = ASTNode.Params; -// No need to export ASTNode.types from the module at this time because (1) it -// wasn't automatically converted by the automatic migration script, (2) the -// name doesn't follow the styleguide. - -/** - * Gets the parent connection on a block. - * This is either an output connection, previous connection or undefined. - * If both connections exist return the one that is actually connected - * to another block. - * - * @param block The block to find the parent connection on. - * @returns The connection connecting to the parent of the block. - */ -function getParentConnection(block: Block): Connection | null { - let topConnection = block.outputConnection; - if ( - !topConnection || - (block.previousConnection && block.previousConnection.isConnected()) - ) { - topConnection = block.previousConnection; - } - return topConnection; -} diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 44c9c0d3887..97b5ae70f4e 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -540,7 +540,7 @@ export class LineCursor extends Marker { for (const node of nodes) { if ( this.validNode(node) && - !this.toASTNode(node)?.getSourceBlock()?.disposed + !this.getSourceBlockFromNode(node)?.disposed ) { this.setCurNode(node); return; diff --git a/core/keyboard_nav/marker.ts b/core/keyboard_nav/marker.ts index 759ae19a7af..931f068b3da 100644 --- a/core/keyboard_nav/marker.ts +++ b/core/keyboard_nav/marker.ts @@ -14,12 +14,8 @@ import {BlockSvg} from '../block_svg.js'; import {Field} from '../field.js'; -import {FlyoutButton} from '../flyout_button.js'; import type {INavigable} from '../interfaces/i_navigable.js'; import {RenderedConnection} from '../rendered_connection.js'; -import {Coordinate} from '../utils/coordinate.js'; -import {WorkspaceSvg} from '../workspace_svg.js'; -import {ASTNode} from './ast_node.js'; /** * Class for a marker. @@ -59,23 +55,17 @@ export class Marker { } /** - * Converts an INavigable to a legacy ASTNode. + * Returns the block that the given node is a child of. * - * @param node The INavigable instance to convert. - * @returns An ASTNode representation of the given object if possible, - * otherwise null. + * @returns The parent block of the node if any, otherwise null. */ - toASTNode(node: INavigable | null): ASTNode | null { + getSourceBlockFromNode(node: INavigable | null): BlockSvg | null { if (node instanceof BlockSvg) { - return ASTNode.createBlockNode(node); + return node; } else if (node instanceof Field) { - return ASTNode.createFieldNode(node); - } else if (node instanceof WorkspaceSvg) { - return ASTNode.createWorkspaceNode(node, new Coordinate(0, 0)); - } else if (node instanceof FlyoutButton) { - return ASTNode.createButtonNode(node); + return node.getSourceBlock() as BlockSvg; } else if (node instanceof RenderedConnection) { - return ASTNode.createConnectionNode(node); + return node.getSourceBlock(); } return null; @@ -88,15 +78,6 @@ export class Marker { * null. */ getSourceBlock(): BlockSvg | null { - const curNode = this.getCurNode(); - if (curNode instanceof BlockSvg) { - return curNode; - } else if (curNode instanceof Field) { - return curNode.getSourceBlock() as BlockSvg; - } else if (curNode instanceof RenderedConnection) { - return curNode.getSourceBlock(); - } - - return null; + return this.getSourceBlockFromNode(this.getCurNode()); } } From 92cad53cfe16419b70f81b44e44e5c92aba59f38 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 8 May 2025 12:47:39 -0700 Subject: [PATCH 195/222] fix!: delete IASTNodeLocation and friends (#9015) --- core/block.ts | 3 +-- core/block_svg.ts | 2 -- core/blockly.ts | 6 ----- core/connection.ts | 3 +-- core/field.ts | 4 ---- core/flyout_button.ts | 2 -- core/interfaces/i_ast_node_location.ts | 12 ---------- core/interfaces/i_ast_node_location_svg.ts | 14 ------------ .../i_ast_node_location_with_block.ts | 22 ------------------- core/workspace.ts | 3 +-- core/workspace_svg.ts | 2 -- 11 files changed, 3 insertions(+), 70 deletions(-) delete mode 100644 core/interfaces/i_ast_node_location.ts delete mode 100644 core/interfaces/i_ast_node_location_svg.ts delete mode 100644 core/interfaces/i_ast_node_location_with_block.ts diff --git a/core/block.ts b/core/block.ts index b95427bce4e..132d19be495 100644 --- a/core/block.ts +++ b/core/block.ts @@ -40,7 +40,6 @@ import {EndRowInput} from './inputs/end_row_input.js'; import {Input} from './inputs/input.js'; import {StatementInput} from './inputs/statement_input.js'; import {ValueInput} from './inputs/value_input.js'; -import type {IASTNodeLocation} from './interfaces/i_ast_node_location.js'; import {isCommentIcon} from './interfaces/i_comment_icon.js'; import {type IIcon} from './interfaces/i_icon.js'; import type { @@ -61,7 +60,7 @@ import type {Workspace} from './workspace.js'; * Class for one block. * Not normally called directly, workspace.newBlock() is preferred. */ -export class Block implements IASTNodeLocation { +export class Block { /** * An optional callback method to use whenever the block's parent workspace * changes. This is usually only called from the constructor, the block type diff --git a/core/block_svg.ts b/core/block_svg.ts index a4171b82635..c2a26e7f86f 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -39,7 +39,6 @@ import {IconType} from './icons/icon_types.js'; import {MutatorIcon} from './icons/mutator_icon.js'; import {WarningIcon} from './icons/warning_icon.js'; import type {Input} from './inputs/input.js'; -import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {ICopyable} from './interfaces/i_copyable.js'; @@ -74,7 +73,6 @@ import type {WorkspaceSvg} from './workspace_svg.js'; export class BlockSvg extends Block implements - IASTNodeLocationSvg, IBoundedElement, IContextMenu, ICopyable, diff --git a/core/blockly.ts b/core/blockly.ts index f250562a4d8..f7b44b2efa5 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -124,9 +124,6 @@ import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.js'; import {Input} from './inputs/input.js'; import {InsertionMarkerPreviewer} from './insertion_marker_previewer.js'; -import {IASTNodeLocation} from './interfaces/i_ast_node_location.js'; -import {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; -import {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js'; import {IAutoHideable} from './interfaces/i_autohideable.js'; import {IBoundedElement} from './interfaces/i_bounded_element.js'; import {IBubble} from './interfaces/i_bubble.js'; @@ -541,9 +538,6 @@ export { Gesture, Grid, HorizontalFlyout, - IASTNodeLocation, - IASTNodeLocationSvg, - IASTNodeLocationWithBlock, IAutoHideable, IBoundedElement, IBubble, diff --git a/core/connection.ts b/core/connection.ts index aed90e7c78c..fbd094dba69 100644 --- a/core/connection.ts +++ b/core/connection.ts @@ -17,7 +17,6 @@ import type {BlockMove} from './events/events_block_move.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import type {Input} from './inputs/input.js'; -import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js'; import type {IConnectionChecker} from './interfaces/i_connection_checker.js'; import * as blocks from './serialization/blocks.js'; import {idGenerator} from './utils.js'; @@ -26,7 +25,7 @@ import * as Xml from './xml.js'; /** * Class for a connection between blocks. */ -export class Connection implements IASTNodeLocationWithBlock { +export class Connection { /** Constants for checking whether two connections are compatible. */ static CAN_CONNECT = 0; static REASON_SELF_CONNECTION = 1; diff --git a/core/field.ts b/core/field.ts index 26fd44df96f..b231b336596 100644 --- a/core/field.ts +++ b/core/field.ts @@ -23,8 +23,6 @@ import * as dropDownDiv from './dropdowndiv.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import type {Input} from './inputs/input.js'; -import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; -import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js'; @@ -71,8 +69,6 @@ export type FieldValidator = (newValue: T) => T | null | undefined; */ export abstract class Field implements - IASTNodeLocationSvg, - IASTNodeLocationWithBlock, IKeyboardAccessible, IRegistrable, ISerializable, diff --git a/core/flyout_button.ts b/core/flyout_button.ts index 257a90bba20..bb104be6746 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -11,7 +11,6 @@ */ // Former goog.module ID: Blockly.FlyoutButton -import type {IASTNodeLocationSvg} from './blockly.js'; import * as browserEvents from './browser_events.js'; import * as Css from './css.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; @@ -34,7 +33,6 @@ import type {WorkspaceSvg} from './workspace_svg.js'; */ export class FlyoutButton implements - IASTNodeLocationSvg, IBoundedElement, IRenderedElement, IFocusableNode, diff --git a/core/interfaces/i_ast_node_location.ts b/core/interfaces/i_ast_node_location.ts deleted file mode 100644 index cc90bbc4065..00000000000 --- a/core/interfaces/i_ast_node_location.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -// Former goog.module ID: Blockly.IASTNodeLocation - -/** - * An AST node location interface. - */ -export interface IASTNodeLocation {} diff --git a/core/interfaces/i_ast_node_location_svg.ts b/core/interfaces/i_ast_node_location_svg.ts deleted file mode 100644 index 60a871e053b..00000000000 --- a/core/interfaces/i_ast_node_location_svg.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -// Former goog.module ID: Blockly.IASTNodeLocationSvg - -import type {IASTNodeLocation} from './i_ast_node_location.js'; - -/** - * An AST node location SVG interface. - */ -export interface IASTNodeLocationSvg extends IASTNodeLocation {} diff --git a/core/interfaces/i_ast_node_location_with_block.ts b/core/interfaces/i_ast_node_location_with_block.ts deleted file mode 100644 index b04234fd4a8..00000000000 --- a/core/interfaces/i_ast_node_location_with_block.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -// Former goog.module ID: Blockly.IASTNodeLocationWithBlock - -import type {Block} from '../block.js'; -import type {IASTNodeLocation} from './i_ast_node_location.js'; - -/** - * An AST node location that has an associated block. - */ -export interface IASTNodeLocationWithBlock extends IASTNodeLocation { - /** - * Get the source block associated with this node. - * - * @returns The source block. - */ - getSourceBlock(): Block | null; -} diff --git a/core/workspace.ts b/core/workspace.ts index 5ae4f8e2b14..32af90c1fa0 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -21,7 +21,6 @@ import * as common from './common.js'; import type {ConnectionDB} from './connection_db.js'; import type {Abstract} from './events/events_abstract.js'; import * as eventUtils from './events/utils.js'; -import type {IASTNodeLocation} from './interfaces/i_ast_node_location.js'; import type {IConnectionChecker} from './interfaces/i_connection_checker.js'; import {IProcedureMap} from './interfaces/i_procedure_map.js'; import type {IVariableMap} from './interfaces/i_variable_map.js'; @@ -43,7 +42,7 @@ import {deleteVariable, getVariableUsesById} from './variables.js'; * Class for a workspace. This is a data structure that contains blocks. * There is no UI, and can be created headlessly. */ -export class Workspace implements IASTNodeLocation { +export class Workspace { /** * Angle away from the horizontal to sweep for blocks. Order of execution is * generally top to bottom, but a small angle changes the scan to give a bit diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 3ff73896ad1..543b099cac1 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -41,7 +41,6 @@ import type {FlyoutButton} from './flyout_button.js'; import {getFocusManager} from './focus_manager.js'; import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; -import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; import {isAutoHideable} from './interfaces/i_autohideable.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import {IContextMenu} from './interfaces/i_contextmenu.js'; @@ -101,7 +100,6 @@ const ZOOM_TO_FIT_MARGIN = 20; export class WorkspaceSvg extends Workspace implements - IASTNodeLocationSvg, IContextMenu, IFocusableNode, IFocusableTree, From 4074cee31bb1cdbebaf465ade020805c1ea99045 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 9 May 2025 08:16:14 -0700 Subject: [PATCH 196/222] feat!: Make everything ISelectable focusable (#9004) * feat!: Make bubbles, comments, and icons focusable * feat!: Make ISelectable and ICopyable focusable. * feat: Consolidate selection calls. Now everything is based on focus with selection only being used as a proxy. * feat: Invert responsibility for setSelected(). Now setSelected() is only for quasi-external use. * feat: Push up shadow check to getters. Needed new common-level helper. * chore: Lint fixes. * feat!: Allow IFocusableNode to disable focus. * chore: post-merge lint fixes * fix: Fix tests + text bubble focusing. This fixed then regressed a circular dependency causing the node and advanced compilation steps to fail. This investigation is ongoing. * fix: Clean up & fix imports. This ensures the node and advanced compilation test steps now pass. * fix: Lint fixes + revert commented out logic. * chore: Remove unnecessary cast. Addresses reviewer comment. * fix: Some issues and a bunch of clean-ups. This addresses a bunch of review comments, and fixes selecting workspace comments. * chore: Lint fix. * fix: Remove unnecessary shadow consideration. * chore: Revert import. * chore: Some doc updates & added a warn statement. --- blocks/procedures.ts | 4 +- core/block_svg.ts | 43 ++++----------- core/bubbles/bubble.ts | 51 ++++++++++++++++-- core/bubbles/textinput_bubble.ts | 56 +++++++------------- core/clipboard/workspace_comment_paster.ts | 4 +- core/comments/rendered_workspace_comment.ts | 36 ++++++++++++- core/common.ts | 42 +++++++++------ core/contextmenu.ts | 4 +- core/contextmenu_items.ts | 4 +- core/css.ts | 6 ++- core/field.ts | 5 ++ core/flyout_base.ts | 7 ++- core/flyout_button.ts | 5 ++ core/focus_manager.ts | 21 +++++--- core/icons/comment_icon.ts | 6 +++ core/icons/icon.ts | 36 ++++++++++++- core/icons/mutator_icon.ts | 6 +++ core/icons/warning_icon.ts | 6 +++ core/interfaces/i_bubble.ts | 3 +- core/interfaces/i_focusable_node.ts | 36 ++++++++++++- core/interfaces/i_has_bubble.ts | 15 ++++++ core/interfaces/i_icon.ts | 6 ++- core/interfaces/i_selectable.ts | 11 +++- core/keyboard_nav/line_cursor.ts | 52 +----------------- core/layer_manager.ts | 7 ++- core/rendered_connection.ts | 24 ++------- core/toolbox/toolbox.ts | 5 ++ core/toolbox/toolbox_item.ts | 5 ++ core/workspace_dragger.ts | 7 +-- core/workspace_svg.ts | 35 +++++++++++- tests/mocha/block_test.js | 15 ------ tests/mocha/focus_manager_test.js | 4 ++ tests/mocha/focusable_tree_traverser_test.js | 4 ++ tests/mocha/test_helpers/icon_mocks.js | 16 ++++++ 34 files changed, 379 insertions(+), 208 deletions(-) diff --git a/blocks/procedures.ts b/blocks/procedures.ts index 7284973d9cc..534bfba6e82 100644 --- a/blocks/procedures.ts +++ b/blocks/procedures.ts @@ -9,7 +9,6 @@ import type {Block} from '../core/block.js'; import type {BlockSvg} from '../core/block_svg.js'; import type {BlockDefinition} from '../core/blocks.js'; -import * as common from '../core/common.js'; import {defineBlocks} from '../core/common.js'; import {config} from '../core/config.js'; import type {Connection} from '../core/connection.js'; @@ -27,6 +26,7 @@ import {FieldCheckbox} from '../core/field_checkbox.js'; import {FieldLabel} from '../core/field_label.js'; import * as fieldRegistry from '../core/field_registry.js'; import {FieldTextInput} from '../core/field_textinput.js'; +import {getFocusManager} from '../core/focus_manager.js'; import '../core/icons/comment_icon.js'; import {MutatorIcon as Mutator} from '../core/icons/mutator_icon.js'; import '../core/icons/warning_icon.js'; @@ -1178,7 +1178,7 @@ const PROCEDURE_CALL_COMMON = { const def = Procedures.getDefinition(name, workspace); if (def) { (workspace as WorkspaceSvg).centerOnBlock(def.id); - common.setSelected(def as BlockSvg); + getFocusManager().focusNode(def as BlockSvg); } }, }); diff --git a/core/block_svg.ts b/core/block_svg.ts index c2a26e7f86f..e4cc27c060a 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -264,20 +264,14 @@ export class BlockSvg /** Selects this block. Highlights the block visually. */ select() { - if (this.isShadow()) { - this.getParent()?.select(); - return; - } this.addSelect(); + common.fireSelectedEvent(this); } /** Unselects this block. Unhighlights the block visually. */ unselect() { - if (this.isShadow()) { - this.getParent()?.unselect(); - return; - } this.removeSelect(); + common.fireSelectedEvent(null); } /** @@ -860,25 +854,6 @@ export class BlockSvg blockAnimations.disposeUiEffect(this); } - // Selecting a shadow block highlights an ancestor block, but that highlight - // should be removed if the shadow block will be deleted. So, before - // deleting blocks and severing the connections between them, check whether - // doing so would delete a selected block and make sure that any associated - // parent is updated. - const selection = common.getSelected(); - if (selection instanceof Block) { - let selectionAncestor: Block | null = selection; - while (selectionAncestor !== null) { - if (selectionAncestor === this) { - // The block to be deleted contains the selected block, so remove any - // selection highlight associated with the selected block before - // deleting them. - selection.unselect(); - } - selectionAncestor = selectionAncestor.getParent(); - } - } - super.dispose(!!healStack); dom.removeNode(this.svgGroup); } @@ -891,8 +866,7 @@ export class BlockSvg this.disposing = true; super.disposeInternal(); - if (common.getSelected() === this) { - this.unselect(); + if (getFocusManager().getFocusedNode() === this) { this.workspace.cancelCurrentGesture(); } @@ -1837,14 +1811,17 @@ export class BlockSvg /** See IFocusableNode.onNodeFocus. */ onNodeFocus(): void { - common.setSelected(this); + this.select(); } /** See IFocusableNode.onNodeBlur. */ onNodeBlur(): void { - if (common.getSelected() === this) { - common.setSelected(null); - } + this.unselect(); + } + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; } /** diff --git a/core/bubbles/bubble.ts b/core/bubbles/bubble.ts index 645e74a60d3..64060fe7888 100644 --- a/core/bubbles/bubble.ts +++ b/core/bubbles/bubble.ts @@ -4,11 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {ISelectable} from '../blockly.js'; import * as browserEvents from '../browser_events.js'; import * as common from '../common.js'; import {BubbleDragStrategy} from '../dragging/bubble_drag_strategy.js'; +import {getFocusManager} from '../focus_manager.js'; import {IBubble} from '../interfaces/i_bubble.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; +import {ISelectable} from '../interfaces/i_selectable.js'; import {ContainerRegion} from '../metrics_manager.js'; import {Scrollbar} from '../scrollbar.js'; import {Coordinate} from '../utils/coordinate.js'; @@ -86,17 +88,24 @@ export abstract class Bubble implements IBubble, ISelectable { private dragStrategy = new BubbleDragStrategy(this, this.workspace); + private focusableElement: SVGElement | HTMLElement; + /** * @param workspace The workspace this bubble belongs to. * @param anchor The anchor location of the thing this bubble is attached to. * The tail of the bubble will point to this location. * @param ownerRect An optional rect we don't want the bubble to overlap with * when automatically positioning. + * @param overriddenFocusableElement An optional replacement to the focusable + * element that's represented by this bubble (as a focusable node). This + * element will have its ID and tabindex overwritten. If not provided, the + * focusable element of this node will default to the bubble's SVG root. */ constructor( public readonly workspace: WorkspaceSvg, protected anchor: Coordinate, protected ownerRect?: Rect, + overriddenFocusableElement?: SVGElement | HTMLElement, ) { this.id = idGenerator.getNextUniqueId(); this.svgRoot = dom.createSvgElement( @@ -127,6 +136,10 @@ export abstract class Bubble implements IBubble, ISelectable { ); this.contentContainer = dom.createSvgElement(Svg.G, {}, this.svgRoot); + this.focusableElement = overriddenFocusableElement ?? this.svgRoot; + this.focusableElement.setAttribute('id', this.id); + this.focusableElement.setAttribute('tabindex', '-1'); + browserEvents.conditionalBind( this.background, 'pointerdown', @@ -208,11 +221,13 @@ export abstract class Bubble implements IBubble, ISelectable { this.background.setAttribute('fill', colour); } - /** Brings the bubble to the front and passes the pointer event off to the gesture system. */ + /** + * Passes the pointer event off to the gesture system and ensures the bubble + * is focused. + */ private onMouseDown(e: PointerEvent) { this.workspace.getGesture(e)?.handleBubbleStart(e, this); - this.bringToFront(); - common.setSelected(this); + getFocusManager().focusNode(this); } /** Positions the bubble relative to its anchor. Does not render its tail. */ @@ -647,9 +662,37 @@ export abstract class Bubble implements IBubble, ISelectable { select(): void { // Bubbles don't have any visual for being selected. + common.fireSelectedEvent(this); } unselect(): void { // Bubbles don't have any visual for being selected. + common.fireSelectedEvent(null); + } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + return this.focusableElement; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.workspace; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void { + this.select(); + this.bringToFront(); + } + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void { + this.unselect(); + } + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; } } diff --git a/core/bubbles/textinput_bubble.ts b/core/bubbles/textinput_bubble.ts index cb13d5caecb..6281ad7584e 100644 --- a/core/bubbles/textinput_bubble.ts +++ b/core/bubbles/textinput_bubble.ts @@ -80,11 +80,10 @@ export class TextInputBubble extends Bubble { protected anchor: Coordinate, protected ownerRect?: Rect, ) { - super(workspace, anchor, ownerRect); + super(workspace, anchor, ownerRect, TextInputBubble.createTextArea()); dom.addClass(this.svgRoot, 'blocklyTextInputBubble'); - ({inputRoot: this.inputRoot, textArea: this.textArea} = this.createEditor( - this.contentContainer, - )); + this.textArea = this.getFocusableElement() as HTMLTextAreaElement; + this.inputRoot = this.createEditor(this.contentContainer, this.textArea); this.resizeGroup = this.createResizeHandle(this.svgRoot, workspace); this.setSize(this.DEFAULT_SIZE, true); } @@ -131,11 +130,21 @@ export class TextInputBubble extends Bubble { this.locationChangeListeners.push(listener); } - /** Creates the editor UI for this bubble. */ - private createEditor(container: SVGGElement): { - inputRoot: SVGForeignObjectElement; - textArea: HTMLTextAreaElement; - } { + /** Creates and returns the editable text area for this bubble's editor. */ + private static createTextArea(): HTMLTextAreaElement { + const textArea = document.createElementNS( + dom.HTML_NS, + 'textarea', + ) as HTMLTextAreaElement; + textArea.className = 'blocklyTextarea blocklyText'; + return textArea; + } + + /** Creates and returns the UI container element for this bubble's editor. */ + private createEditor( + container: SVGGElement, + textArea: HTMLTextAreaElement, + ): SVGForeignObjectElement { const inputRoot = dom.createSvgElement( Svg.FOREIGNOBJECT, { @@ -149,22 +158,13 @@ export class TextInputBubble extends Bubble { body.setAttribute('xmlns', dom.HTML_NS); body.className = 'blocklyMinimalBody'; - const textArea = document.createElementNS( - dom.HTML_NS, - 'textarea', - ) as HTMLTextAreaElement; - textArea.className = 'blocklyTextarea blocklyText'; textArea.setAttribute('dir', this.workspace.RTL ? 'RTL' : 'LTR'); - body.appendChild(textArea); inputRoot.appendChild(body); this.bindTextAreaEvents(textArea); - setTimeout(() => { - textArea.focus(); - }, 0); - return {inputRoot, textArea}; + return inputRoot; } /** Binds events to the text area element. */ @@ -174,13 +174,6 @@ export class TextInputBubble extends Bubble { e.stopPropagation(); }); - browserEvents.conditionalBind( - textArea, - 'focus', - this, - this.onStartEdit, - true, - ); browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange); } @@ -314,17 +307,6 @@ export class TextInputBubble extends Bubble { this.onSizeChange(); } - /** - * Handles starting an edit of the text area. Brings the bubble to the front. - */ - private onStartEdit() { - if (this.bringToFront()) { - // Since the act of moving this node within the DOM causes a loss of - // focus, we need to reapply the focus. - this.textArea.focus(); - } - } - /** Handles a text change event for the text area. Calls event listeners. */ private onTextChange() { this.text = this.textArea.value; diff --git a/core/clipboard/workspace_comment_paster.ts b/core/clipboard/workspace_comment_paster.ts index fdfbf0a8419..00c56681dd3 100644 --- a/core/clipboard/workspace_comment_paster.ts +++ b/core/clipboard/workspace_comment_paster.ts @@ -5,9 +5,9 @@ */ import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js'; -import * as common from '../common.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; +import {getFocusManager} from '../focus_manager.js'; import {ICopyData} from '../interfaces/i_copyable.js'; import {IPaster} from '../interfaces/i_paster.js'; import * as commentSerialiation from '../serialization/workspace_comments.js'; @@ -49,7 +49,7 @@ export class WorkspaceCommentPaster if (eventUtils.isEnabled()) { eventUtils.fire(new (eventUtils.get(EventType.COMMENT_CREATE))(comment)); } - common.setSelected(comment); + getFocusManager().focusNode(comment); return comment; } } diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index bcb650b26ff..76eeb64f16a 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -13,11 +13,13 @@ import * as common from '../common.js'; import * as contextMenu from '../contextmenu.js'; import {ContextMenuRegistry} from '../contextmenu_registry.js'; import {CommentDragStrategy} from '../dragging/comment_drag_strategy.js'; +import {getFocusManager} from '../focus_manager.js'; import {IBoundedElement} from '../interfaces/i_bounded_element.js'; import {IContextMenu} from '../interfaces/i_contextmenu.js'; import {ICopyable} from '../interfaces/i_copyable.js'; import {IDeletable} from '../interfaces/i_deletable.js'; import {IDraggable} from '../interfaces/i_draggable.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import {IRenderedElement} from '../interfaces/i_rendered_element.js'; import {ISelectable} from '../interfaces/i_selectable.js'; import * as layers from '../layers.js'; @@ -60,6 +62,8 @@ export class RenderedWorkspaceComment this.view.setSize(this.getSize()); this.view.setEditable(this.isEditable()); this.view.getSvgRoot().setAttribute('data-id', this.id); + this.view.getSvgRoot().setAttribute('id', this.id); + this.view.getSvgRoot().setAttribute('tabindex', '-1'); this.addModelUpdateBindings(); @@ -220,9 +224,8 @@ export class RenderedWorkspaceComment e.stopPropagation(); } else { gesture.handleCommentStart(e, this); - this.workspace.getLayerManager()?.append(this, layers.BLOCK); } - common.setSelected(this); + getFocusManager().focusNode(this); } } @@ -263,11 +266,13 @@ export class RenderedWorkspaceComment /** Visually highlights the comment. */ select(): void { dom.addClass(this.getSvgRoot(), 'blocklySelected'); + common.fireSelectedEvent(this); } /** Visually unhighlights the comment. */ unselect(): void { dom.removeClass(this.getSvgRoot(), 'blocklySelected'); + common.fireSelectedEvent(null); } /** @@ -322,4 +327,31 @@ export class RenderedWorkspaceComment this.moveTo(alignedXY, ['snap']); } } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + return this.getSvgRoot(); + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.workspace; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void { + this.select(); + // Ensure that the comment is always at the top when focused. + this.workspace.getLayerManager()?.append(this, layers.BLOCK); + } + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void { + this.unselect(); + } + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } } diff --git a/core/common.ts b/core/common.ts index bc31bf17eea..1f7ba7e88df 100644 --- a/core/common.ts +++ b/core/common.ts @@ -7,11 +7,12 @@ // Former goog.module ID: Blockly.common import type {Block} from './block.js'; -import {ISelectable} from './blockly.js'; import {BlockDefinition, Blocks} from './blocks.js'; import type {Connection} from './connection.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; +import {getFocusManager} from './focus_manager.js'; +import {ISelectable, isSelectable} from './interfaces/i_selectable.js'; import type {Workspace} from './workspace.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -86,38 +87,45 @@ export function setMainWorkspace(workspace: Workspace) { } /** - * Currently selected copyable object. + * Returns the current selection. */ -let selected: ISelectable | null = null; +export function getSelected(): ISelectable | null { + const focused = getFocusManager().getFocusedNode(); + if (focused && isSelectable(focused)) return focused; + return null; +} /** - * Returns the currently selected copyable object. + * Sets the current selection. + * + * To clear the current selection, select another ISelectable or focus a + * non-selectable (like the workspace root node). + * + * @param newSelection The new selection to make. + * @internal */ -export function getSelected(): ISelectable | null { - return selected; +export function setSelected(newSelection: ISelectable) { + getFocusManager().focusNode(newSelection); } /** - * Sets the currently selected block. This function does not visually mark the - * block as selected or fire the required events. If you wish to - * programmatically select a block, use `BlockSvg#select`. + * Fires a selection change event based on the new selection. + * + * This is only expected to be called by ISelectable implementations and should + * always be called before updating the current selection state. It does not + * change focus or selection state. * - * @param newSelection The newly selected block. + * @param newSelection The new selection. * @internal */ -export function setSelected(newSelection: ISelectable | null) { - if (selected === newSelection) return; - +export function fireSelectedEvent(newSelection: ISelectable | null) { + const selected = getSelected(); const event = new (eventUtils.get(EventType.SELECTED))( selected?.id ?? null, newSelection?.id ?? null, newSelection?.workspace.id ?? selected?.workspace.id ?? '', ); eventUtils.fire(event); - - selected?.unselect(); - selected = newSelection; - selected?.select(); } /** diff --git a/core/contextmenu.ts b/core/contextmenu.ts index 4ba09de8231..f3ebbd1c681 100644 --- a/core/contextmenu.ts +++ b/core/contextmenu.ts @@ -9,7 +9,6 @@ import type {Block} from './block.js'; import type {BlockSvg} from './block_svg.js'; import * as browserEvents from './browser_events.js'; -import * as common from './common.js'; import {config} from './config.js'; import type { ContextMenuOption, @@ -17,6 +16,7 @@ import type { } from './contextmenu_registry.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; +import {getFocusManager} from './focus_manager.js'; import {Menu} from './menu.js'; import {MenuSeparator} from './menu_separator.js'; import {MenuItem} from './menuitem.js'; @@ -289,7 +289,7 @@ export function callbackFactory( if (eventUtils.isEnabled() && !newBlock.isShadow()) { eventUtils.fire(new (eventUtils.get(EventType.BLOCK_CREATE))(newBlock)); } - common.setSelected(newBlock); + getFocusManager().focusNode(newBlock); return newBlock; }; } diff --git a/core/contextmenu_items.ts b/core/contextmenu_items.ts index 267305e2121..774bfdde29b 100644 --- a/core/contextmenu_items.ts +++ b/core/contextmenu_items.ts @@ -9,7 +9,6 @@ import type {BlockSvg} from './block_svg.js'; import * as clipboard from './clipboard.js'; import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; -import * as common from './common.js'; import {MANUALLY_DISABLED} from './constants.js'; import { ContextMenuRegistry, @@ -19,6 +18,7 @@ import { import * as dialog from './dialog.js'; import * as Events from './events/events.js'; import * as eventUtils from './events/utils.js'; +import {getFocusManager} from './focus_manager.js'; import {CommentIcon} from './icons/comment_icon.js'; import {Msg} from './msg.js'; import {StatementInput} from './renderers/zelos/zelos.js'; @@ -631,7 +631,7 @@ export function registerCommentCreate() { workspace, ), ); - common.setSelected(comment); + getFocusManager().focusNode(comment); eventUtils.setGroup(false); }, scopeType: ContextMenuRegistry.ScopeType.WORKSPACE, diff --git a/core/css.ts b/core/css.ts index e1a2a10d307..6b5e19a585b 100644 --- a/core/css.ts +++ b/core/css.ts @@ -499,7 +499,11 @@ input[type=number] { .blocklyWorkspace, .blocklyField, .blocklyPath, - .blocklyHighlightedConnectionPath + .blocklyHighlightedConnectionPath, + .blocklyComment, + .blocklyBubble, + .blocklyIconGroup, + .blocklyTextarea ) { outline-width: 0px; } diff --git a/core/field.ts b/core/field.ts index b231b336596..589817b19d7 100644 --- a/core/field.ts +++ b/core/field.ts @@ -1364,6 +1364,11 @@ export abstract class Field /** See IFocusableNode.onNodeBlur. */ onNodeBlur(): void {} + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } + /** * Subclasses should reimplement this method to construct their Field * subclass from a JSON arg object. diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 82ae0c23d4d..2af2808205c 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -26,7 +26,7 @@ import {IAutoHideable} from './interfaces/i_autohideable.js'; import type {IFlyout} from './interfaces/i_flyout.js'; import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import {IFocusableNode} from './interfaces/i_focusable_node.js'; -import {IFocusableTree} from './interfaces/i_focusable_tree.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import * as renderManagement from './render_management.js'; @@ -986,6 +986,11 @@ export abstract class Flyout /** See IFocusableNode.onNodeBlur. */ onNodeBlur(): void {} + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } + /** See IFocusableTree.getRootFocusableNode. */ getRootFocusableNode(): IFocusableNode { return this; diff --git a/core/flyout_button.ts b/core/flyout_button.ts index bb104be6746..6790145a7b4 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -408,6 +408,11 @@ export class FlyoutButton /** See IFocusableNode.onNodeBlur. */ onNodeBlur(): void {} + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } + /** * Returns whether or not this button is accessible through keyboard * navigation. diff --git a/core/focus_manager.ts b/core/focus_manager.ts index 4c5fab16043..c0139aec08d 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -81,7 +81,7 @@ export class FocusManager { } } - if (newNode) { + if (newNode && newNode.canBeFocused()) { const newTree = newNode.getFocusableTree(); const oldTree = this.focusedNode?.getFocusableTree(); if (newNode === newTree.getRootFocusableNode() && newTree !== oldTree) { @@ -232,11 +232,20 @@ export class FocusManager { * Any previously focused node will be updated to be passively highlighted (if * it's in a different focusable tree) or blurred (if it's in the same one). * + * **Important**: If the provided node is not able to be focused (e.g. its + * canBeFocused() method returns false), it will be ignored and any existing + * focus state will remain unchanged. + * * @param focusableNode The node that should receive active focus. */ focusNode(focusableNode: IFocusableNode): void { this.ensureManagerIsUnlocked(); if (this.focusedNode === focusableNode) return; // State is unchanged. + if (!focusableNode.canBeFocused()) { + // This node can't be focused. + console.warn("Trying to focus a node that can't be focused."); + return; + } const nextTree = focusableNode.getFocusableTree(); if (!this.isRegistered(nextTree)) { @@ -395,9 +404,9 @@ export class FocusManager { } /** - * Marks the specified node as actively focused, also calling related lifecycle - * callback methods for both the node and its parent tree. This ensures that - * the node is properly styled to indicate its active focus. + * Marks the specified node as actively focused, also calling related + * lifecycle callback methods for both the node and its parent tree. This + * ensures that the node is properly styled to indicate its active focus. * * This does not change the manager's currently tracked node, nor does it * change any other nodes. @@ -494,8 +503,8 @@ export class FocusManager { /** * Returns the page-global FocusManager. * - * The returned instance is guaranteed to not change across function calls, but - * may change across page loads. + * The returned instance is guaranteed to not change across function calls, + * but may change across page loads. */ static getFocusManager(): FocusManager { if (!FocusManager.focusManager) { diff --git a/core/icons/comment_icon.ts b/core/icons/comment_icon.ts index b546769647e..959eb2500f5 100644 --- a/core/icons/comment_icon.ts +++ b/core/icons/comment_icon.ts @@ -11,6 +11,7 @@ import type {BlockSvg} from '../block_svg.js'; import {TextInputBubble} from '../bubbles/textinput_bubble.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; +import type {IBubble} from '../interfaces/i_bubble.js'; import type {IHasBubble} from '../interfaces/i_has_bubble.js'; import type {ISerializable} from '../interfaces/i_serializable.js'; import * as renderManagement from '../render_management.js'; @@ -338,6 +339,11 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { ); } + /** See IHasBubble.getBubble. */ + getBubble(): IBubble | null { + return this.textInputBubble; + } + /** * Shows the editable text bubble for this comment, and adds change listeners * to update the state of this icon in response to changes in the bubble. diff --git a/core/icons/icon.ts b/core/icons/icon.ts index 30a6b538f6e..5bf61d49c91 100644 --- a/core/icons/icon.ts +++ b/core/icons/icon.ts @@ -7,13 +7,16 @@ import type {Block} from '../block.js'; import type {BlockSvg} from '../block_svg.js'; import * as browserEvents from '../browser_events.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import {hasBubble} from '../interfaces/i_has_bubble.js'; import type {IIcon} from '../interfaces/i_icon.js'; import * as tooltip from '../tooltip.js'; import {Coordinate} from '../utils/coordinate.js'; import * as dom from '../utils/dom.js'; +import * as idGenerator from '../utils/idgenerator.js'; import {Size} from '../utils/size.js'; import {Svg} from '../utils/svg.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; import type {IconType} from './icon_types.js'; /** @@ -38,8 +41,12 @@ export abstract class Icon implements IIcon { /** The tooltip for this icon. */ protected tooltip: tooltip.TipInfo; + /** The unique ID of this icon. */ + private id: string; + constructor(protected sourceBlock: Block) { this.tooltip = sourceBlock; + this.id = idGenerator.getNextUniqueId(); } getType(): IconType { @@ -50,7 +57,11 @@ export abstract class Icon implements IIcon { if (this.svgRoot) return; // The icon has already been initialized. const svgBlock = this.sourceBlock as BlockSvg; - this.svgRoot = dom.createSvgElement(Svg.G, {'class': 'blocklyIconGroup'}); + this.svgRoot = dom.createSvgElement(Svg.G, { + 'class': 'blocklyIconGroup', + 'tabindex': '-1', + 'id': this.id, + }); svgBlock.getSvgRoot().appendChild(this.svgRoot); this.updateSvgRootOffset(); browserEvents.conditionalBind( @@ -144,4 +155,27 @@ export abstract class Icon implements IIcon { isClickableInFlyout(autoClosingFlyout: boolean): boolean { return true; } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + const svgRoot = this.svgRoot; + if (!svgRoot) throw new Error('Attempting to focus uninitialized icon.'); + return svgRoot; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.sourceBlock.workspace as WorkspaceSvg; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } } diff --git a/core/icons/mutator_icon.ts b/core/icons/mutator_icon.ts index d8d91bea98b..1842855fab5 100644 --- a/core/icons/mutator_icon.ts +++ b/core/icons/mutator_icon.ts @@ -14,6 +14,7 @@ import {BlockChange} from '../events/events_block_change.js'; import {isBlockChange, isBlockCreate} from '../events/predicates.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; +import type {IBubble} from '../interfaces/i_bubble.js'; import type {IHasBubble} from '../interfaces/i_has_bubble.js'; import * as renderManagement from '../render_management.js'; import {Coordinate} from '../utils/coordinate.js'; @@ -203,6 +204,11 @@ export class MutatorIcon extends Icon implements IHasBubble { ); } + /** See IHasBubble.getBubble. */ + getBubble(): IBubble | null { + return this.miniWorkspaceBubble; + } + /** @returns the configuration the mini workspace should have. */ private getMiniWorkspaceConfig() { const options: BlocklyOptions = { diff --git a/core/icons/warning_icon.ts b/core/icons/warning_icon.ts index 87d932bb5fe..f24a6a56190 100644 --- a/core/icons/warning_icon.ts +++ b/core/icons/warning_icon.ts @@ -10,6 +10,7 @@ import type {BlockSvg} from '../block_svg.js'; import {TextBubble} from '../bubbles/text_bubble.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; +import type {IBubble} from '../interfaces/i_bubble.js'; import type {IHasBubble} from '../interfaces/i_has_bubble.js'; import * as renderManagement from '../render_management.js'; import {Size} from '../utils.js'; @@ -197,6 +198,11 @@ export class WarningIcon extends Icon implements IHasBubble { ); } + /** See IHasBubble.getBubble. */ + getBubble(): IBubble | null { + return this.textBubble; + } + /** * @returns the location the bubble should be anchored to. * I.E. the middle of this icon. diff --git a/core/interfaces/i_bubble.ts b/core/interfaces/i_bubble.ts index d31ce9c9dce..553f86e9e9e 100644 --- a/core/interfaces/i_bubble.ts +++ b/core/interfaces/i_bubble.ts @@ -9,11 +9,12 @@ import type {Coordinate} from '../utils/coordinate.js'; import type {IContextMenu} from './i_contextmenu.js'; import type {IDraggable} from './i_draggable.js'; +import {IFocusableNode} from './i_focusable_node.js'; /** * A bubble interface. */ -export interface IBubble extends IDraggable, IContextMenu { +export interface IBubble extends IDraggable, IContextMenu, IFocusableNode { /** * Return the coordinates of the top-left corner of this bubble's body * relative to the drawing surface's origin (0,0), in workspace units. diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts index 53a432d30f4..6844e080941 100644 --- a/core/interfaces/i_focusable_node.ts +++ b/core/interfaces/i_focusable_node.ts @@ -33,6 +33,9 @@ export interface IFocusableNode { * It's expected the actual returned element will not change for the lifetime * of the node (that is, its properties can change but a new element should * never be returned). + * + * @returns The HTMLElement or SVGElement which can both receive focus and be + * visually represented as actively or passively focused for this node. */ getFocusableElement(): HTMLElement | SVGElement; @@ -40,6 +43,8 @@ export interface IFocusableNode { * Returns the closest parent tree of this node (in cases where a tree has * distinct trees underneath it), which represents the tree to which this node * belongs. + * + * @returns The node's IFocusableTree. */ getFocusableTree(): IFocusableTree; @@ -59,6 +64,34 @@ export interface IFocusableNode { * This has the same implementation restrictions as onNodeFocus(). */ onNodeBlur(): void; + + /** + * Indicates whether this node allows focus. If this returns false then none + * of the other IFocusableNode methods will be called. + * + * Note that special care must be taken if implementations of this function + * dynamically change their return value value over the lifetime of the node + * as certain environment conditions could affect the focusability of this + * node's DOM element (such as whether the element has a positive or zero + * tabindex). Also, changing from a true to a false value while the node holds + * focus will not immediately change the current focus of the node nor + * FocusManager's internal state, and thus may result in some of the node's + * functions being called later on when defocused (since it was previously + * considered focusable at the time of being focused). + * + * Implementations should generally always return true here unless there are + * circumstances under which this node should be skipped for focus + * considerations. Examples may include being disabled, read-only, a purely + * visual decoration, or a node with no visual representation that must + * implement this interface (e.g. due to a parent interface extending it). + * Keep in mind accessibility best practices when determining whether a node + * should be focusable since even disabled and read-only elements are still + * often relevant to providing organizational context to users (particularly + * when using a screen reader). + * + * @returns Whether this node can be focused by FocusManager. + */ + canBeFocused(): boolean; } /** @@ -74,6 +107,7 @@ export function isFocusableNode(object: any | null): object is IFocusableNode { 'getFocusableElement' in object && 'getFocusableTree' in object && 'onNodeFocus' in object && - 'onNodeBlur' in object + 'onNodeBlur' in object && + 'canBeFocused' in object ); } diff --git a/core/interfaces/i_has_bubble.ts b/core/interfaces/i_has_bubble.ts index 276feff21e2..85c6f099031 100644 --- a/core/interfaces/i_has_bubble.ts +++ b/core/interfaces/i_has_bubble.ts @@ -4,12 +4,27 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type {IBubble} from './i_bubble'; + export interface IHasBubble { /** @returns True if the bubble is currently open, false otherwise. */ bubbleIsVisible(): boolean; /** Sets whether the bubble is open or not. */ setBubbleVisible(visible: boolean): Promise; + + /** + * Returns the current IBubble that implementations are managing, or null if + * there isn't one. + * + * Note that this cannot be expected to return null if bubbleIsVisible() + * returns false, i.e., the nullability of the returned bubble does not + * necessarily imply visibility. + * + * @returns The current IBubble maintained by implementations, or null if + * there is not one. + */ + getBubble(): IBubble | null; } /** Type guard that checks whether the given object is a IHasBubble. */ diff --git a/core/interfaces/i_icon.ts b/core/interfaces/i_icon.ts index a6159985f91..74489dc5e09 100644 --- a/core/interfaces/i_icon.ts +++ b/core/interfaces/i_icon.ts @@ -7,8 +7,9 @@ import type {IconType} from '../icons/icon_types.js'; import type {Coordinate} from '../utils/coordinate.js'; import type {Size} from '../utils/size.js'; +import {IFocusableNode, isFocusableNode} from './i_focusable_node.js'; -export interface IIcon { +export interface IIcon extends IFocusableNode { /** * @returns the IconType representing the type of the icon. This value should * also be used to register the icon via `Blockly.icons.registry.register`. @@ -109,6 +110,7 @@ export function isIcon(obj: any): obj is IIcon { obj.isShownWhenCollapsed !== undefined && obj.setOffsetInBlock !== undefined && obj.onLocationChange !== undefined && - obj.onClick !== undefined + obj.onClick !== undefined && + isFocusableNode(obj) ); } diff --git a/core/interfaces/i_selectable.ts b/core/interfaces/i_selectable.ts index 972b0adb107..639972e45cb 100644 --- a/core/interfaces/i_selectable.ts +++ b/core/interfaces/i_selectable.ts @@ -7,11 +7,17 @@ // Former goog.module ID: Blockly.ISelectable import type {Workspace} from '../workspace.js'; +import {IFocusableNode, isFocusableNode} from './i_focusable_node.js'; /** * The interface for an object that is selectable. + * + * Implementations are generally expected to use their implementations of + * onNodeFocus() and onNodeBlur() to call setSelected() with themselves and + * null, respectively, in order to ensure that selections are correctly updated + * and the selection change event is fired. */ -export interface ISelectable { +export interface ISelectable extends IFocusableNode { id: string; workspace: Workspace; @@ -29,6 +35,7 @@ export function isSelectable(obj: object): obj is ISelectable { typeof (obj as any).id === 'string' && (obj as any).workspace !== undefined && (obj as any).select !== undefined && - (obj as any).unselect !== undefined + (obj as any).unselect !== undefined && + isFocusableNode(obj) ); } diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 97b5ae70f4e..1dcfdd8a295 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -14,7 +14,6 @@ */ import {BlockSvg} from '../block_svg.js'; -import * as common from '../common.js'; import {ConnectionType} from '../connection_type.js'; import {Field} from '../field.js'; import {FieldCheckbox} from '../field_checkbox.js'; @@ -561,12 +560,7 @@ export class LineCursor extends Marker { * @returns The current field, connection, or block the cursor is on. */ override getCurNode(): INavigable | null { - if (!this.updateCurNodeFromFocus()) { - // Fall back to selection if focus fails to sync. This can happen for - // non-focusable nodes or for cases when focus may not properly propagate - // (such as for mouse clicks). - this.updateCurNodeFromSelection(); - } + this.updateCurNodeFromFocus(); return super.getCurNode(); } @@ -593,60 +587,18 @@ export class LineCursor extends Marker { } } - /** - * Updates the current node to match the selection. - * - * Clears the current node if it's on a block but the selection is null. - * Sets the node to a block if selected for our workspace. - * For shadow blocks selections the parent is used by default (unless we're - * already on the shadow block via keyboard) as that's where the visual - * selection is. - */ - private updateCurNodeFromSelection() { - const curNode = super.getCurNode(); - const selected = common.getSelected(); - - if (selected === null && curNode instanceof BlockSvg) { - this.setCurNode(null); - return; - } - if (selected?.workspace !== this.workspace) { - return; - } - if (selected instanceof BlockSvg) { - let block: BlockSvg | null = selected; - if (selected.isShadow()) { - // OK if the current node is on the parent OR the shadow block. - // The former happens for clicks, the latter for keyboard nav. - if (curNode && (curNode === block || curNode === block.getParent())) { - return; - } - block = block.getParent(); - } - if (block) { - this.setCurNode(block); - } - } - } - /** * Updates the current node to match what's currently focused. - * - * @returns Whether the current node has been set successfully from the - * current focused node. */ - private updateCurNodeFromFocus(): boolean { + private updateCurNodeFromFocus() { const focused = getFocusManager().getFocusedNode(); if (focused instanceof BlockSvg) { const block: BlockSvg | null = focused; if (block && block.workspace === this.workspace) { this.setCurNode(block); - return true; } } - - return false; } /** diff --git a/core/layer_manager.ts b/core/layer_manager.ts index e7663b1b7ee..a7cb579348f 100644 --- a/core/layer_manager.ts +++ b/core/layer_manager.ts @@ -122,7 +122,12 @@ export class LayerManager { if (!this.layers.has(layerNum)) { this.createLayer(layerNum); } - this.layers.get(layerNum)?.appendChild(elem.getSvgRoot()); + const childElem = elem.getSvgRoot(); + if (this.layers.get(layerNum)?.lastChild !== childElem) { + // Only append the child if it isn't already last (to avoid re-firing + // events like focused). + this.layers.get(layerNum)?.appendChild(childElem); + } } /** diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index 858bc3d1d35..ec1b4ed0a48 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -13,7 +13,6 @@ import type {Block} from './block.js'; import type {BlockSvg} from './block_svg.js'; -import * as common from './common.js'; import {config} from './config.js'; import {Connection} from './connection.js'; import type {ConnectionDB} from './connection_db.js'; @@ -198,15 +197,12 @@ export class RenderedConnection ? inferiorRootBlock : superiorRootBlock; // Raise it to the top for extra visibility. - const selected = common.getSelected() === dynamicRootBlock; - if (!selected) dynamicRootBlock.addSelect(); if (dynamicRootBlock.RTL) { offsetX = -offsetX; } const dx = staticConnection.x + offsetX - dynamicConnection.x; const dy = staticConnection.y + offsetY - dynamicConnection.y; dynamicRootBlock.moveBy(dx, dy, ['bump']); - if (!selected) dynamicRootBlock.removeSelect(); } /** @@ -559,21 +555,6 @@ export class RenderedConnection childBlock.updateDisabled(); childBlock.queueRender(); - // If either block being connected was selected, visually un- and reselect - // it. This has the effect of moving the selection path to the end of the - // list of child nodes in the DOM. Since SVG z-order is determined by node - // order in the DOM, this works around an issue where the selection outline - // path could be partially obscured by a new block inserted after it in the - // DOM. - const selection = common.getSelected(); - const selectedBlock = - (selection === parentBlock && parentBlock) || - (selection === childBlock && childBlock); - if (selectedBlock) { - selectedBlock.removeSelect(); - selectedBlock.addSelect(); - } - // The input the child block is connected to (if any). const parentInput = parentBlock.getInputWithBlock(childBlock); if (parentInput) { @@ -671,6 +652,11 @@ export class RenderedConnection this.unhighlight(); } + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } + private findHighlightSvg(): SVGElement | null { // This cast is valid as TypeScript's definition is wrong. See: // https://github.com/microsoft/TypeScript/issues/60996. diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 650630e6a86..0fbb231dc56 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -1094,6 +1094,11 @@ export class Toolbox /** See IFocusableNode.onNodeBlur. */ onNodeBlur(): void {} + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } + /** See IFocusableTree.getRootFocusableNode. */ getRootFocusableNode(): IFocusableNode { return this; diff --git a/core/toolbox/toolbox_item.ts b/core/toolbox/toolbox_item.ts index 0d46a5eadfd..9fc5c160ddc 100644 --- a/core/toolbox/toolbox_item.ts +++ b/core/toolbox/toolbox_item.ts @@ -172,5 +172,10 @@ export class ToolboxItem implements IToolboxItem { /** See IFocusableNode.onNodeBlur. */ onNodeBlur(): void {} + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } } // nop by default diff --git a/core/workspace_dragger.ts b/core/workspace_dragger.ts index 7ad5651f791..312015e8556 100644 --- a/core/workspace_dragger.ts +++ b/core/workspace_dragger.ts @@ -11,7 +11,6 @@ */ // Former goog.module ID: Blockly.WorkspaceDragger -import * as common from './common.js'; import {Coordinate} from './utils/coordinate.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -56,11 +55,7 @@ export class WorkspaceDragger { * * @internal */ - startDrag() { - if (common.getSelected()) { - common.getSelected()!.unselect(); - } - } + startDrag() {} /** * Finish dragging the workspace and put everything back where it belongs. diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 543b099cac1..2dae154e05d 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -51,6 +51,7 @@ import { type IFocusableNode, } from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; +import {hasBubble} from './interfaces/i_has_bubble.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; import type {INavigable} from './interfaces/i_navigable.js'; import type {IToolbox} from './interfaces/i_toolbox.js'; @@ -2694,6 +2695,11 @@ export class WorkspaceSvg /** See IFocusableNode.onNodeBlur. */ onNodeBlur(): void {} + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } + /** See IFocusableTree.getRootFocusableNode. */ getRootFocusableNode(): IFocusableNode { return this; @@ -2728,6 +2734,7 @@ export class WorkspaceSvg } } + // Search for fields and connections (based on ID indicators). const fieldIndicatorIndex = id.indexOf('_field_'); const connectionIndicatorIndex = id.indexOf('_connection_'); if (fieldIndicatorIndex !== -1) { @@ -2750,7 +2757,33 @@ export class WorkspaceSvg return null; } - return this.getBlockById(id) as IFocusableNode; + // Search for a specific block. + const block = this.getBlockById(id); + if (block) return block; + + // Search for a workspace comment (semi-expensive). + for (const comment of this.getTopComments()) { + if ( + comment instanceof RenderedWorkspaceComment && + comment.getFocusableElement().id === id + ) { + return comment; + } + } + + // Search for icons and bubbles (which requires an expensive getAllBlocks). + const icons = this.getAllBlocks() + .map((block) => block.getIcons()) + .flat(); + for (const icon of icons) { + if (icon.getFocusableElement().id === id) return icon; + if (hasBubble(icon)) { + const bubble = icon.getBubble(); + if (bubble && bubble.getFocusableElement().id === id) return bubble; + } + } + + return null; } /** See IFocusableTree.onTreeFocus. */ diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index 874e5161cee..eda2d82a56d 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as common from '../../build/src/core/common.js'; import {ConnectionType} from '../../build/src/core/connection_type.js'; import {EventType} from '../../build/src/core/events/type.js'; import * as eventUtils from '../../build/src/core/events/utils.js'; @@ -463,20 +462,6 @@ suite('Blocks', function () { teardown(function () { workspaceTeardown.call(this, this.workspace); }); - - test('Disposing selected shadow unhighlights parent', function () { - const parentBlock = this.parentBlock; - common.setSelected(this.shadowChild); - assert.isTrue( - parentBlock.pathObject.svgRoot.classList.contains('blocklySelected'), - 'Expected parent to be highlighted after selecting shadow child', - ); - this.shadowChild.dispose(); - assert.isFalse( - parentBlock.pathObject.svgRoot.classList.contains('blocklySelected'), - 'Expected parent to be unhighlighted after deleting shadow child', - ); - }); }); }); diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js index 264d91734c1..b1cfb029a87 100644 --- a/tests/mocha/focus_manager_test.js +++ b/tests/mocha/focus_manager_test.js @@ -31,6 +31,10 @@ class FocusableNodeImpl { onNodeFocus() {} onNodeBlur() {} + + canBeFocused() { + return true; + } } class FocusableTreeImpl { diff --git a/tests/mocha/focusable_tree_traverser_test.js b/tests/mocha/focusable_tree_traverser_test.js index d2467b6e95c..66cc598ccf5 100644 --- a/tests/mocha/focusable_tree_traverser_test.js +++ b/tests/mocha/focusable_tree_traverser_test.js @@ -29,6 +29,10 @@ class FocusableNodeImpl { onNodeFocus() {} onNodeBlur() {} + + canBeFocused() { + return true; + } } class FocusableTreeImpl { diff --git a/tests/mocha/test_helpers/icon_mocks.js b/tests/mocha/test_helpers/icon_mocks.js index 039c4082f5c..5d117c71284 100644 --- a/tests/mocha/test_helpers/icon_mocks.js +++ b/tests/mocha/test_helpers/icon_mocks.js @@ -34,6 +34,22 @@ export class MockIcon { onLocationChange() {} onClick() {} + + getFocusableElement() { + throw new Error('Unsupported operation in mock.'); + } + + getFocusableTree() { + throw new Error('Unsupported operation in mock.'); + } + + onNodeFocus() {} + + onNodeBlur() {} + + canBeFocused() { + return false; + } } export class MockSerializableIcon extends MockIcon { From 9cf91708151a4be7383db9f90634099009bcfd24 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Fri, 9 May 2025 16:20:36 -0700 Subject: [PATCH 197/222] chore: remove now-unneeded cast (#9016) --- core/contextmenu_registry.ts | 3 +-- core/shortcut_registry.ts | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/contextmenu_registry.ts b/core/contextmenu_registry.ts index fc7a94dcb08..61fac7a719f 100644 --- a/core/contextmenu_registry.ts +++ b/core/contextmenu_registry.ts @@ -155,8 +155,7 @@ export namespace ContextMenuRegistry { block?: BlockSvg; workspace?: WorkspaceSvg; comment?: RenderedWorkspaceComment; - // TODO(#8839): Remove any once Block, etc. implement IFocusableNode - focusedNode?: IFocusableNode | any; + focusedNode?: IFocusableNode; } /** diff --git a/core/shortcut_registry.ts b/core/shortcut_registry.ts index b819cbbf748..8a276c3d51c 100644 --- a/core/shortcut_registry.ts +++ b/core/shortcut_registry.ts @@ -253,7 +253,7 @@ export class ShortcutRegistry { !shortcut || (shortcut.preconditionFn && !shortcut.preconditionFn(workspace, { - focusedNode: getFocusManager().getFocusedNode(), + focusedNode: getFocusManager().getFocusedNode() ?? undefined, })) ) { continue; @@ -261,10 +261,11 @@ export class ShortcutRegistry { // If the key has been handled, stop processing shortcuts. if ( shortcut.callback?.(workspace, e, shortcut, { - focusedNode: getFocusManager().getFocusedNode(), + focusedNode: getFocusManager().getFocusedNode() ?? undefined, }) - ) + ) { return true; + } } return false; } From 40ec75be44304c87742f8f0b92ba73f0d51b1686 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 08:36:22 -0700 Subject: [PATCH 198/222] chore(deps): bump eslint-plugin-prettier from 5.2.3 to 5.4.0 (#9024) Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.2.3 to 5.4.0. - [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases) - [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.2.3...v5.4.0) --- updated-dependencies: - dependency-name: eslint-plugin-prettier dependency-version: 5.4.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 47 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 294d8149d4b..24480f70d89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4326,14 +4326,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", - "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz", + "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "synckit": "^0.11.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -4344,7 +4344,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -4356,6 +4356,43 @@ } } }, + "node_modules/eslint-plugin-prettier/node_modules/@pkgr/core": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", + "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/eslint-plugin-prettier/node_modules/synckit": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", + "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/eslint-plugin-prettier/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/eslint-scope": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", From 77bfa5b5729a371d23da8cc149ab01dac7d1feba Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Mon, 12 May 2025 10:29:53 -0700 Subject: [PATCH 199/222] fix: Don't fire events for changes to potential variables (#9025) --- core/variable_map.ts | 57 +++++++++++++++++++++++++++++++------------- core/workspace.ts | 2 +- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/core/variable_map.ts b/core/variable_map.ts index 44386d2f7d4..403893332e5 100644 --- a/core/variable_map.ts +++ b/core/variable_map.ts @@ -46,8 +46,15 @@ export class VariableMap Map> >(); - /** @param workspace The workspace this map belongs to. */ - constructor(public workspace: Workspace) {} + /** + * @param workspace The workspace this map belongs to. + * @param potentialMap True if this holds variables that don't exist in the + * workspace yet. + */ + constructor( + public workspace: Workspace, + public potentialMap = false, + ) {} /** Clear the variable map. Fires events for every deletion. */ clear() { @@ -77,9 +84,12 @@ export class VariableMap const type = variable.getType(); const conflictVar = this.getVariable(newName, type); const blocks = this.workspace.getAllBlocks(false); - const existingGroup = eventUtils.getGroup(); - if (!existingGroup) { - eventUtils.setGroup(true); + let existingGroup = ''; + if (!this.potentialMap) { + existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } } try { // The IDs may match if the rename is a simple case change (name1 -> @@ -90,7 +100,7 @@ export class VariableMap this.renameVariableWithConflict(variable, newName, conflictVar, blocks); } } finally { - eventUtils.setGroup(existingGroup); + if (!this.potentialMap) eventUtils.setGroup(existingGroup); } return variable; } @@ -147,9 +157,11 @@ export class VariableMap newName: string, blocks: Block[], ) { - eventUtils.fire( - new (eventUtils.get(EventType.VAR_RENAME))(variable, newName), - ); + if (!this.potentialMap) { + eventUtils.fire( + new (eventUtils.get(EventType.VAR_RENAME))(variable, newName), + ); + } variable.setName(newName); for (let i = 0; i < blocks.length; i++) { blocks[i].updateVarName(variable); @@ -186,8 +198,10 @@ export class VariableMap for (let i = 0; i < blocks.length; i++) { blocks[i].renameVarById(variable.getId(), conflictVar.getId()); } - // Finally delete the original variable, which is now unreferenced. - eventUtils.fire(new (eventUtils.get(EventType.VAR_DELETE))(variable)); + if (!this.potentialMap) { + // Finally delete the original variable, which is now unreferenced. + eventUtils.fire(new (eventUtils.get(EventType.VAR_DELETE))(variable)); + } // And remove it from the map. this.variableMap.get(type)?.delete(variable.getId()); } @@ -248,7 +262,9 @@ export class VariableMap if (!this.variableMap.has(type)) { this.variableMap.set(type, variables); } - eventUtils.fire(new (eventUtils.get(EventType.VAR_CREATE))(variable)); + if (!this.potentialMap) { + eventUtils.fire(new (eventUtils.get(EventType.VAR_CREATE))(variable)); + } return variable; } @@ -276,9 +292,12 @@ export class VariableMap */ deleteVariable(variable: IVariableModel) { const uses = getVariableUsesById(this.workspace, variable.getId()); - const existingGroup = eventUtils.getGroup(); - if (!existingGroup) { - eventUtils.setGroup(true); + let existingGroup = ''; + if (!this.potentialMap) { + existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } } try { for (let i = 0; i < uses.length; i++) { @@ -287,12 +306,16 @@ export class VariableMap const variables = this.variableMap.get(variable.getType()); if (!variables || !variables.has(variable.getId())) return; variables.delete(variable.getId()); - eventUtils.fire(new (eventUtils.get(EventType.VAR_DELETE))(variable)); + if (!this.potentialMap) { + eventUtils.fire(new (eventUtils.get(EventType.VAR_DELETE))(variable)); + } if (variables.size === 0) { this.variableMap.delete(variable.getType()); } } finally { - eventUtils.setGroup(existingGroup); + if (!this.potentialMap) { + eventUtils.setGroup(existingGroup); + } } } diff --git a/core/workspace.ts b/core/workspace.ts index 32af90c1fa0..f7b866447c4 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -868,7 +868,7 @@ export class Workspace { */ createPotentialVariableMap() { const VariableMap = this.getVariableMapClass(); - this.potentialVariableMap = new VariableMap(this); + this.potentialVariableMap = new VariableMap(this, true); } /** From d9e5d95f02a68d160eda9fa7905754abb0821752 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 11:22:01 -0700 Subject: [PATCH 200/222] chore(deps): bump globals from 16.0.0 to 16.1.0 (#9023) Bumps [globals](https://github.com/sindresorhus/globals) from 16.0.0 to 16.1.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v16.0.0...v16.1.0) --- updated-dependencies: - dependency-name: globals dependency-version: 16.1.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 24480f70d89..547eda9bb10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5453,9 +5453,9 @@ } }, "node_modules/globals": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", - "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", + "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", "dev": true, "license": "MIT", "engines": { From a1be83bad8c753b4aec6d1d421ab48608f216fff Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 12 May 2025 15:46:27 -0700 Subject: [PATCH 201/222] refactor: Make INavigable extend IFocusableNode. (#9033) --- core/block_svg.ts | 9 ------- core/field.ts | 14 ----------- core/flyout_button.ts | 10 -------- core/flyout_separator.ts | 25 ++++++++++++++++++- core/interfaces/i_navigable.ts | 18 +++---------- core/interfaces/i_navigation_policy.ts | 14 +++++++++++ core/keyboard_nav/block_navigation_policy.ts | 10 ++++++++ .../connection_navigation_policy.ts | 10 ++++++++ core/keyboard_nav/field_navigation_policy.ts | 19 ++++++++++++++ .../flyout_button_navigation_policy.ts | 10 ++++++++ core/keyboard_nav/flyout_navigation_policy.ts | 10 ++++++++ .../flyout_separator_navigation_policy.ts | 10 ++++++++ .../workspace_navigation_policy.ts | 10 ++++++++ core/navigator.ts | 14 ++++++++--- core/rendered_connection.ts | 9 ------- core/workspace_svg.ts | 15 ++++------- 16 files changed, 135 insertions(+), 72 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index e4cc27c060a..1ec4d98eafe 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -1824,15 +1824,6 @@ export class BlockSvg return true; } - /** - * Returns whether or not this block can be navigated to via the keyboard. - * - * @returns True if this block is keyboard navigable, otherwise false. - */ - isNavigable() { - return true; - } - /** * Returns this block's class. * diff --git a/core/field.ts b/core/field.ts index 589817b19d7..7aa348d52c3 100644 --- a/core/field.ts +++ b/core/field.ts @@ -1385,20 +1385,6 @@ export abstract class Field ); } - /** - * Returns whether or not this field is accessible by keyboard navigation. - * - * @returns True if this field is keyboard accessible, otherwise false. - */ - isNavigable() { - return ( - this.isClickable() && - this.isCurrentlyEditable() && - !(this.getSourceBlock()?.isSimpleReporter() && this.isFullBlockField()) && - this.getParentInput().isVisible() - ); - } - /** * Returns this field's class. * diff --git a/core/flyout_button.ts b/core/flyout_button.ts index 6790145a7b4..605a1358535 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -413,16 +413,6 @@ export class FlyoutButton return true; } - /** - * Returns whether or not this button is accessible through keyboard - * navigation. - * - * @returns True if this button is keyboard accessible, otherwise false. - */ - isNavigable() { - return true; - } - /** * Returns this button's class. * diff --git a/core/flyout_separator.ts b/core/flyout_separator.ts index 02737879a7c..73698b3dd6c 100644 --- a/core/flyout_separator.ts +++ b/core/flyout_separator.ts @@ -5,6 +5,8 @@ */ import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {INavigable} from './interfaces/i_navigable.js'; import {Rect} from './utils/rect.js'; @@ -12,7 +14,7 @@ import {Rect} from './utils/rect.js'; * Representation of a gap between elements in a flyout. */ export class FlyoutSeparator - implements IBoundedElement, INavigable + implements IBoundedElement, INavigable, IFocusableNode { private x = 0; private y = 0; @@ -75,6 +77,27 @@ export class FlyoutSeparator getClass() { return FlyoutSeparator; } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + throw new Error('Cannot be focused'); + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + throw new Error('Cannot be focused'); + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return false; + } } /** diff --git a/core/interfaces/i_navigable.ts b/core/interfaces/i_navigable.ts index f41e7882934..dcb8ad50fca 100644 --- a/core/interfaces/i_navigable.ts +++ b/core/interfaces/i_navigable.ts @@ -4,24 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type {IFocusableNode} from './i_focusable_node.js'; + /** * Represents a UI element which can be navigated to using the keyboard. */ -export interface INavigable { - /** - * Returns whether or not this specific instance should be reachable via - * keyboard navigation. - * - * Implementors should generally return true, unless there are circumstances - * under which this item should be skipped while using keyboard navigation. - * Common examples might include being disabled, invalid, readonly, or purely - * a visual decoration. For example, while Fields are navigable, non-editable - * fields return false, since they cannot be interacted with when focused. - * - * @returns True if this element should be included in keyboard navigation. - */ - isNavigable(): boolean; - +export interface INavigable extends IFocusableNode { /** * Returns the class of this instance. * diff --git a/core/interfaces/i_navigation_policy.ts b/core/interfaces/i_navigation_policy.ts index b263aac5987..d2b694c897a 100644 --- a/core/interfaces/i_navigation_policy.ts +++ b/core/interfaces/i_navigation_policy.ts @@ -43,4 +43,18 @@ export interface INavigationPolicy { * there is none. */ getPreviousSibling(current: T): INavigable | null; + + /** + * Returns whether or not the given instance should be reachable via keyboard + * navigation. + * + * Implementors should generally return true, unless there are circumstances + * under which this item should be skipped while using keyboard navigation. + * Common examples might include being disabled, invalid, readonly, or purely + * a visual decoration. For example, while Fields are navigable, non-editable + * fields return false, since they cannot be interacted with when focused. + * + * @returns True if this element should be included in keyboard navigation. + */ + isNavigable(current: T): boolean; } diff --git a/core/keyboard_nav/block_navigation_policy.ts b/core/keyboard_nav/block_navigation_policy.ts index b073253f98e..aac16c2a449 100644 --- a/core/keyboard_nav/block_navigation_policy.ts +++ b/core/keyboard_nav/block_navigation_policy.ts @@ -114,4 +114,14 @@ export class BlockNavigationPolicy implements INavigationPolicy { } return block.outputConnection; } + + /** + * Returns whether or not the given block can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given block can be focused. + */ + isNavigable(current: BlockSvg): boolean { + return current.canBeFocused(); + } } diff --git a/core/keyboard_nav/connection_navigation_policy.ts b/core/keyboard_nav/connection_navigation_policy.ts index 1dfb0f64edd..8323a73e148 100644 --- a/core/keyboard_nav/connection_navigation_policy.ts +++ b/core/keyboard_nav/connection_navigation_policy.ts @@ -166,4 +166,14 @@ export class ConnectionNavigationPolicy } return block.outputConnection; } + + /** + * Returns whether or not the given connection can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given connection can be focused. + */ + isNavigable(current: RenderedConnection): boolean { + return current.canBeFocused(); + } } diff --git a/core/keyboard_nav/field_navigation_policy.ts b/core/keyboard_nav/field_navigation_policy.ts index a0e43001e4f..d4470e51788 100644 --- a/core/keyboard_nav/field_navigation_policy.ts +++ b/core/keyboard_nav/field_navigation_policy.ts @@ -88,4 +88,23 @@ export class FieldNavigationPolicy implements INavigationPolicy> { } return null; } + + /** + * Returns whether or not the given field can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given field can be focused and navigated to. + */ + isNavigable(current: Field): boolean { + return ( + current.canBeFocused() && + current.isClickable() && + current.isCurrentlyEditable() && + !( + current.getSourceBlock()?.isSimpleReporter() && + current.isFullBlockField() + ) && + current.getParentInput().isVisible() + ); + } } diff --git a/core/keyboard_nav/flyout_button_navigation_policy.ts b/core/keyboard_nav/flyout_button_navigation_policy.ts index 8819bf588c6..771a327b589 100644 --- a/core/keyboard_nav/flyout_button_navigation_policy.ts +++ b/core/keyboard_nav/flyout_button_navigation_policy.ts @@ -53,4 +53,14 @@ export class FlyoutButtonNavigationPolicy getPreviousSibling(_current: FlyoutButton): INavigable | null { return null; } + + /** + * Returns whether or not the given flyout button can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given flyout button can be focused. + */ + isNavigable(current: FlyoutButton): boolean { + return current.canBeFocused(); + } } diff --git a/core/keyboard_nav/flyout_navigation_policy.ts b/core/keyboard_nav/flyout_navigation_policy.ts index 6a89a6fbdc6..1abdbfb6cd8 100644 --- a/core/keyboard_nav/flyout_navigation_policy.ts +++ b/core/keyboard_nav/flyout_navigation_policy.ts @@ -88,4 +88,14 @@ export class FlyoutNavigationPolicy implements INavigationPolicy { return flyoutContents[index].getElement(); } + + /** + * Returns whether or not the given flyout item can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given flyout item can be focused. + */ + isNavigable(current: T): boolean { + return this.policy.isNavigable(current); + } } diff --git a/core/keyboard_nav/flyout_separator_navigation_policy.ts b/core/keyboard_nav/flyout_separator_navigation_policy.ts index d104c4c0074..2e69af37a66 100644 --- a/core/keyboard_nav/flyout_separator_navigation_policy.ts +++ b/core/keyboard_nav/flyout_separator_navigation_policy.ts @@ -30,4 +30,14 @@ export class FlyoutSeparatorNavigationPolicy getPreviousSibling(_current: FlyoutSeparator): INavigable | null { return null; } + + /** + * Returns whether or not the given flyout separator can be navigated to. + * + * @param _current The instance to check for navigability. + * @returns False. + */ + isNavigable(_current: FlyoutSeparator): boolean { + return false; + } } diff --git a/core/keyboard_nav/workspace_navigation_policy.ts b/core/keyboard_nav/workspace_navigation_policy.ts index 91f3221b558..656b0453b86 100644 --- a/core/keyboard_nav/workspace_navigation_policy.ts +++ b/core/keyboard_nav/workspace_navigation_policy.ts @@ -63,4 +63,14 @@ export class WorkspaceNavigationPolicy getPreviousSibling(_current: WorkspaceSvg): INavigable | null { return null; } + + /** + * Returns whether or not the given workspace can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given workspace can be focused. + */ + isNavigable(current: WorkspaceSvg): boolean { + return current.canBeFocused(); + } } diff --git a/core/navigator.ts b/core/navigator.ts index 33e98c47d79..ffff2507c3a 100644 --- a/core/navigator.ts +++ b/core/navigator.ts @@ -59,7 +59,9 @@ export class Navigator { const result = this.get(current)?.getFirstChild(current); if (!result) return null; // If the child isn't navigable, don't traverse into it; check its peers. - if (!result.isNavigable()) return this.getNextSibling(result); + if (!this.get(result)?.isNavigable(result)) { + return this.getNextSibling(result); + } return result; } @@ -72,7 +74,7 @@ export class Navigator { getParent>(current: T): INavigable | null { const result = this.get(current)?.getParent(current); if (!result) return null; - if (!result.isNavigable()) return this.getParent(result); + if (!this.get(result)?.isNavigable(result)) return this.getParent(result); return result; } @@ -85,7 +87,9 @@ export class Navigator { getNextSibling>(current: T): INavigable | null { const result = this.get(current)?.getNextSibling(current); if (!result) return null; - if (!result.isNavigable()) return this.getNextSibling(result); + if (!this.get(result)?.isNavigable(result)) { + return this.getNextSibling(result); + } return result; } @@ -100,7 +104,9 @@ export class Navigator { ): INavigable | null { const result = this.get(current)?.getPreviousSibling(current); if (!result) return null; - if (!result.isNavigable()) return this.getPreviousSibling(result); + if (!this.get(result)?.isNavigable(result)) { + return this.getPreviousSibling(result); + } return result; } } diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index ec1b4ed0a48..4298afa3f23 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -665,15 +665,6 @@ export class RenderedConnection | null as SVGElement | null; } - /** - * Returns whether or not this connection is keyboard-navigable. - * - * @returns True. - */ - isNavigable() { - return true; - } - /** * Returns this connection's class for keyboard navigation. * diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 2dae154e05d..dd8cac242d2 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2728,7 +2728,11 @@ export class WorkspaceSvg if (this.isFlyout && flyout) { for (const flyoutItem of flyout.getContents()) { const elem = flyoutItem.getElement(); - if (isFocusableNode(elem) && elem.getFocusableElement().id === id) { + if ( + isFocusableNode(elem) && + elem.canBeFocused() && + elem.getFocusableElement().id === id + ) { return elem; } } @@ -2817,15 +2821,6 @@ export class WorkspaceSvg return WorkspaceSvg; } - /** - * Returns whether or not this workspace is keyboard-navigable. - * - * @returns True. - */ - isNavigable() { - return true; - } - /** * Returns an object responsible for coordinating movement of focus between * items on this workspace in response to keyboard navigation commands. From e7af75e051858dccf5efc317852788a69013cb14 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 12 May 2025 16:36:23 -0700 Subject: [PATCH 202/222] fix: Improve robustness of `IFocusableNode` uses (#9031) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes https://github.com/google/blockly-keyboard-experimentation/issues/515 ### Proposed Changes This adds `canBeFocused()` checks to all the places that could currently cause problems if a node were to return `false`. ### Reason for Changes This can't introduce a problem in current Core and, in fact, most of these classes can never return `false` even through subclasses. However, this adds better robustness and fixes the underlying issue by ensuring that `getFocusableElement()` isn't called for a node that has indicated it cannot be focused. ### Test Coverage I've manually tested this through the keyboard navigation plugin. However, there are clearly additional tests that would be nice to add both for the traverser and for `WorkspaceSvg`, both likely as part of resolving #8915. ### Documentation No new documentation changes should be needed here. ### Additional Information This is fixing theoretical issues in Core, but a real issue tracked by the keyboard navigation plugin repository. --- core/utils/focusable_tree_traverser.ts | 11 +++++++---- core/workspace_svg.ts | 17 ++++++++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/core/utils/focusable_tree_traverser.ts b/core/utils/focusable_tree_traverser.ts index 94603edd01b..916437b6a73 100644 --- a/core/utils/focusable_tree_traverser.ts +++ b/core/utils/focusable_tree_traverser.ts @@ -32,13 +32,15 @@ export class FocusableTreeTraverser { * @returns The IFocusableNode currently with focus, or null if none. */ static findFocusedNode(tree: IFocusableTree): IFocusableNode | null { - const root = tree.getRootFocusableNode().getFocusableElement(); + const rootNode = tree.getRootFocusableNode(); + if (!rootNode.canBeFocused()) return null; + const root = rootNode.getFocusableElement(); if ( dom.hasClass(root, FocusableTreeTraverser.ACTIVE_CLASS_NAME) || dom.hasClass(root, FocusableTreeTraverser.PASSIVE_CSS_CLASS_NAME) ) { // The root has focus. - return tree.getRootFocusableNode(); + return rootNode; } const activeEl = root.querySelector(this.ACTIVE_FOCUS_NODE_CSS_SELECTOR); @@ -99,8 +101,9 @@ export class FocusableTreeTraverser { } // Second, check against the tree's root. - if (element === tree.getRootFocusableNode().getFocusableElement()) { - return tree.getRootFocusableNode(); + const rootNode = tree.getRootFocusableNode(); + if (rootNode.canBeFocused() && element === rootNode.getFocusableElement()) { + return rootNode; } // Third, check if the element has a node. diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index dd8cac242d2..3095f092474 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2746,7 +2746,9 @@ export class WorkspaceSvg const block = this.getBlockById(blockId); if (block) { for (const field of block.getFields()) { - if (field.getFocusableElement().id === id) return field; + if (field.canBeFocused() && field.getFocusableElement().id === id) { + return field; + } } } return null; @@ -2769,6 +2771,7 @@ export class WorkspaceSvg for (const comment of this.getTopComments()) { if ( comment instanceof RenderedWorkspaceComment && + comment.canBeFocused() && comment.getFocusableElement().id === id ) { return comment; @@ -2780,10 +2783,18 @@ export class WorkspaceSvg .map((block) => block.getIcons()) .flat(); for (const icon of icons) { - if (icon.getFocusableElement().id === id) return icon; + if (icon.canBeFocused() && icon.getFocusableElement().id === id) { + return icon; + } if (hasBubble(icon)) { const bubble = icon.getBubble(); - if (bubble && bubble.getFocusableElement().id === id) return bubble; + if ( + bubble && + bubble.canBeFocused() && + bubble.getFocusableElement().id === id + ) { + return bubble; + } } } From ece662a45f5a6ee480cda70037dbd5b0b4ccf3ca Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 13 May 2025 11:03:01 -0700 Subject: [PATCH 203/222] Fix: don't visit connections with the cursor. (#9030) --- core/keyboard_nav/block_navigation_policy.ts | 126 +++++++----- core/keyboard_nav/field_navigation_policy.ts | 9 +- core/keyboard_nav/line_cursor.ts | 191 +++++------------- .../workspace_navigation_policy.ts | 11 +- tests/mocha/cursor_test.js | 119 ++++++----- tests/mocha/navigation_test.js | 61 +++--- 6 files changed, 220 insertions(+), 297 deletions(-) diff --git a/core/keyboard_nav/block_navigation_policy.ts b/core/keyboard_nav/block_navigation_policy.ts index aac16c2a449..55c7086ec9f 100644 --- a/core/keyboard_nav/block_navigation_policy.ts +++ b/core/keyboard_nav/block_navigation_policy.ts @@ -4,10 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {BlockSvg} from '../block_svg.js'; +import {BlockSvg} from '../block_svg.js'; +import type {Field} from '../field.js'; import type {INavigable} from '../interfaces/i_navigable.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; -import type {RenderedConnection} from '../rendered_connection.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; /** * Set of rules controlling keyboard navigation from a block. @@ -24,7 +25,8 @@ export class BlockNavigationPolicy implements INavigationPolicy { for (const field of input.fieldRow) { return field; } - if (input.connection) return input.connection as RenderedConnection; + if (input.connection?.targetBlock()) + return input.connection.targetBlock() as BlockSvg; } return null; @@ -38,12 +40,14 @@ export class BlockNavigationPolicy implements INavigationPolicy { * which it is attached. */ getParent(current: BlockSvg): INavigable | null { - const topBlock = current.getTopStackBlock(); + if (current.previousConnection?.targetBlock()) { + const surroundParent = current.getSurroundParent(); + if (surroundParent) return surroundParent; + } else if (current.outputConnection?.targetBlock()) { + return current.outputConnection.targetBlock(); + } - return ( - (this.getParentConnection(topBlock)?.targetConnection?.getParentInput() - ?.connection as RenderedConnection) ?? topBlock - ); + return current.workspace; } /** @@ -54,21 +58,40 @@ export class BlockNavigationPolicy implements INavigationPolicy { * block, or its next connection. */ getNextSibling(current: BlockSvg): INavigable | null { - const nextConnection = current.nextConnection; - if (!current.outputConnection?.targetConnection && !nextConnection) { - // If this block has no connected output connection and no next - // connection, it must be the last block in the stack, so its next sibling - // is the first block of the next stack on the workspace. - const topBlocks = current.workspace.getTopBlocks(true); - let targetIndex = topBlocks.indexOf(current.getRootBlock()) + 1; - if (targetIndex >= topBlocks.length) { - targetIndex = 0; + if (current.nextConnection?.targetBlock()) { + return current.nextConnection?.targetBlock(); + } + + const parent = this.getParent(current); + let navigatingCrossStacks = false; + let siblings: (BlockSvg | Field)[] = []; + if (parent instanceof BlockSvg) { + for (let i = 0, input; (input = parent.inputList[i]); i++) { + if (input.connection) { + siblings.push(...input.fieldRow); + const child = input.connection.targetBlock(); + if (child) { + siblings.push(child as BlockSvg); + } + } } - const previousBlock = topBlocks[targetIndex]; - return this.getParentConnection(previousBlock) ?? previousBlock; + } else if (parent instanceof WorkspaceSvg) { + siblings = parent.getTopBlocks(true); + navigatingCrossStacks = true; + } else { + return null; } - return nextConnection; + const currentIndex = siblings.indexOf( + navigatingCrossStacks ? current.getRootBlock() : current, + ); + if (currentIndex >= 0 && currentIndex < siblings.length - 1) { + return siblings[currentIndex + 1]; + } else if (currentIndex === siblings.length - 1 && navigatingCrossStacks) { + return siblings[0]; + } + + return null; } /** @@ -79,40 +102,45 @@ export class BlockNavigationPolicy implements INavigationPolicy { * connection/block of the previous block stack if it is a root block. */ getPreviousSibling(current: BlockSvg): INavigable | null { - const parentConnection = this.getParentConnection(current); - if (parentConnection) return parentConnection; - - // If this block has no output/previous connection, it must be a root block, - // so its previous sibling is the last connection of the last block of the - // previous stack on the workspace. - const topBlocks = current.workspace.getTopBlocks(true); - let targetIndex = topBlocks.indexOf(current.getRootBlock()) - 1; - if (targetIndex < 0) { - targetIndex = topBlocks.length - 1; + if (current.previousConnection?.targetBlock()) { + return current.previousConnection?.targetBlock(); } - const lastBlock = topBlocks[targetIndex] - .getDescendants(true) - .reverse() - .pop(); + const parent = this.getParent(current); + let navigatingCrossStacks = false; + let siblings: (BlockSvg | Field)[] = []; + if (parent instanceof BlockSvg) { + for (let i = 0, input; (input = parent.inputList[i]); i++) { + if (input.connection) { + siblings.push(...input.fieldRow); + const child = input.connection.targetBlock(); + if (child) { + siblings.push(child as BlockSvg); + } + } + } + } else if (parent instanceof WorkspaceSvg) { + siblings = parent.getTopBlocks(true); + navigatingCrossStacks = true; + } else { + return null; + } - return lastBlock?.nextConnection ?? lastBlock ?? null; - } + const currentIndex = siblings.indexOf(current); + let result: INavigable | null = null; + if (currentIndex >= 1) { + result = siblings[currentIndex - 1]; + } else if (currentIndex === 0 && navigatingCrossStacks) { + result = siblings[siblings.length - 1]; + } - /** - * Gets the parent connection on a block. - * This is either an output connection, previous connection or undefined. - * If both connections exist return the one that is actually connected - * to another block. - * - * @param block The block to find the parent connection on. - * @returns The connection connecting to the parent of the block. - */ - protected getParentConnection(block: BlockSvg) { - if (!block.outputConnection || block.previousConnection?.isConnected()) { - return block.previousConnection; + // If navigating to a previous stack, our previous sibling is the last + // block in it. + if (navigatingCrossStacks && result instanceof BlockSvg) { + return result.lastConnectionInStack(false)?.getSourceBlock() ?? result; } - return block.outputConnection; + + return result; } /** diff --git a/core/keyboard_nav/field_navigation_policy.ts b/core/keyboard_nav/field_navigation_policy.ts index d4470e51788..a4a48df503a 100644 --- a/core/keyboard_nav/field_navigation_policy.ts +++ b/core/keyboard_nav/field_navigation_policy.ts @@ -8,7 +8,6 @@ import type {BlockSvg} from '../block_svg.js'; import type {Field} from '../field.js'; import type {INavigable} from '../interfaces/i_navigable.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; -import type {RenderedConnection} from '../rendered_connection.js'; /** * Set of rules controlling keyboard navigation from a field. @@ -52,8 +51,8 @@ export class FieldNavigationPolicy implements INavigationPolicy> { const fieldRow = newInput.fieldRow; if (fieldIdx < fieldRow.length) return fieldRow[fieldIdx]; fieldIdx = 0; - if (newInput.connection) { - return newInput.connection as RenderedConnection; + if (newInput.connection?.targetBlock()) { + return newInput.connection.targetBlock() as BlockSvg; } } return null; @@ -74,8 +73,8 @@ export class FieldNavigationPolicy implements INavigationPolicy> { let fieldIdx = parentInput.fieldRow.indexOf(current) - 1; for (let i = curIdx; i >= 0; i--) { const input = block.inputList[i]; - if (input.connection && input !== parentInput) { - return input.connection as RenderedConnection; + if (input.connection?.targetBlock() && input !== parentInput) { + return input.connection.targetBlock() as BlockSvg; } const fieldRow = input.fieldRow; if (fieldIdx > -1) return fieldRow[fieldIdx]; diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 1dcfdd8a295..845fab9519c 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -14,8 +14,6 @@ */ import {BlockSvg} from '../block_svg.js'; -import {ConnectionType} from '../connection_type.js'; -import {Field} from '../field.js'; import {FieldCheckbox} from '../field_checkbox.js'; import {FieldDropdown} from '../field_dropdown.js'; import {FieldImage} from '../field_image.js'; @@ -146,7 +144,12 @@ export class LineCursor extends Marker { } const newNode = this.getNextNode( curNode, - this.workspace.isFlyout ? () => true : this.validLineNode.bind(this), + (candidate: INavigable | null) => { + return ( + candidate instanceof BlockSvg && + !candidate.outputConnection?.targetBlock() + ); + }, true, ); @@ -168,11 +171,8 @@ export class LineCursor extends Marker { if (!curNode) { return null; } - const newNode = this.getNextNode( - curNode, - this.workspace.isFlyout ? () => true : this.validInLineNode.bind(this), - true, - ); + + const newNode = this.getNextNode(curNode, () => true, true); if (newNode) { this.setCurNode(newNode); @@ -193,7 +193,12 @@ export class LineCursor extends Marker { } const newNode = this.getPreviousNode( curNode, - this.workspace.isFlyout ? () => true : this.validLineNode.bind(this), + (candidate: INavigable | null) => { + return ( + candidate instanceof BlockSvg && + !candidate.outputConnection?.targetBlock() + ); + }, true, ); @@ -215,11 +220,8 @@ export class LineCursor extends Marker { if (!curNode) { return null; } - const newNode = this.getPreviousNode( - curNode, - this.workspace.isFlyout ? () => true : this.validInLineNode.bind(this), - true, - ); + + const newNode = this.getPreviousNode(curNode, () => true, true); if (newNode) { this.setCurNode(newNode); @@ -229,102 +231,26 @@ export class LineCursor extends Marker { /** * Returns true iff the node to which we would navigate if in() were - * called, which will be a validInLineNode, is also a validLineNode - * - in effect, if the LineCursor is at the end of the 'current + * called is the same as the node to which we would navigate if next() were + * called - in effect, if the LineCursor is at the end of the 'current * line' of the program. */ atEndOfLine(): boolean { const curNode = this.getCurNode(); if (!curNode) return false; - const rightNode = this.getNextNode( + const inNode = this.getNextNode(curNode, () => true, true); + const nextNode = this.getNextNode( curNode, - this.validInLineNode.bind(this), - false, + (candidate: INavigable | null) => { + return ( + candidate instanceof BlockSvg && + !candidate.outputConnection?.targetBlock() + ); + }, + true, ); - return this.validLineNode(rightNode); - } - /** - * Returns true iff the given node represents the "beginning of a - * new line of code" (and thus can be visited by pressing the - * up/down arrow keys). Specifically, if the node is for: - * - * - Any block that is not a value block. - * - A top-level value block (one that is unconnected). - * - An unconnected next statement input. - * - An unconnected 'next' connection - the "blank line at the end". - * This is to facilitate connecting additional blocks to a - * stack/substack. - * - * If options.stackConnections is true (the default) then allow the - * cursor to visit all (useful) stack connection by additionally - * returning true for: - * - * - Any next statement input - * - Any 'next' connection. - * - An unconnected previous statement input. - * - * @param node The AST node to check. - * @returns True if the node should be visited, false otherwise. - */ - protected validLineNode(node: INavigable | null): boolean { - if (!node) return false; - - if (node instanceof BlockSvg) { - return !node.outputConnection?.isConnected(); - } else if (node instanceof RenderedConnection) { - if (node.type === ConnectionType.NEXT_STATEMENT) { - return this.options.stackConnections || !node.isConnected(); - } else if (node.type === ConnectionType.PREVIOUS_STATEMENT) { - return this.options.stackConnections && !node.isConnected(); - } - } - - return false; - } - - /** - * Returns true iff the given node can be visited by the cursor when - * using the left/right arrow keys. Specifically, if the node is - * any node for which validLineNode would return true, plus: - * - * - Any block. - * - Any field that is not a full block field. - * - Any unconnected next or input connection. This is to - * facilitate connecting additional blocks. - * - * @param node The AST node to check whether it is valid. - * @returns True if the node should be visited, false otherwise. - */ - protected validInLineNode(node: INavigable | null): boolean { - if (!node) return false; - if (this.validLineNode(node)) return true; - if (node instanceof BlockSvg || node instanceof Field) { - return true; - } else if ( - node instanceof RenderedConnection && - node.getParentInput() && - (node.type === ConnectionType.INPUT_VALUE || - node.type === ConnectionType.NEXT_STATEMENT) - ) { - return !node.isConnected(); - } - - return false; - } - - /** - * Returns true iff the given node can be visited by the cursor. - * Specifically, if the node is any for which validInLineNode would - * return true, or if it is a workspace node. - * - * @param node The AST node to check whether it is valid. - * @returns True if the node should be visited, false otherwise. - */ - protected validNode(node: INavigable | null): boolean { - return ( - !!node && (this.validInLineNode(node) || node instanceof WorkspaceSvg) - ); + return inNode === nextNode; } /** @@ -347,15 +273,15 @@ export class LineCursor extends Marker { let newNode = this.workspace.getNavigator().getFirstChild(node) || this.workspace.getNavigator().getNextSibling(node); - if (isValid(newNode)) return newNode; - if (newNode) { - visitedNodes.add(node); - return this.getNextNodeImpl(newNode, isValid, visitedNodes); + + let target = node; + while (target && !newNode) { + const parent = this.workspace.getNavigator().getParent(target); + if (!parent) break; + newNode = this.workspace.getNavigator().getNextSibling(parent); + target = parent; } - newNode = this.findSiblingOrParentSibling( - this.workspace.getNavigator().getParent(node), - ); if (isValid(newNode)) return newNode; if (newNode) { visitedNodes.add(node); @@ -402,15 +328,12 @@ export class LineCursor extends Marker { visitedNodes: Set> = new Set>(), ): INavigable | null { if (!node || visitedNodes.has(node)) return null; - let newNode: INavigable | null = this.workspace - .getNavigator() - .getPreviousSibling(node); - if (newNode) { - newNode = this.getRightMostChild(newNode); - } else { - newNode = this.workspace.getNavigator().getParent(node); - } + const newNode = + this.getRightMostChild( + this.workspace.getNavigator().getPreviousSibling(node), + node, + ) || this.workspace.getNavigator().getParent(node); if (isValid(newNode)) return newNode; if (newNode) { @@ -441,24 +364,6 @@ export class LineCursor extends Marker { return this.getPreviousNodeImpl(node, isValid); } - /** - * From the given node find either the next valid sibling or the parent's - * next sibling. - * - * @param node The current position in the AST. - * @returns The next sibling node, the parent's next sibling, or null. - */ - private findSiblingOrParentSibling( - node: INavigable | null, - ): INavigable | null { - if (!node) return null; - const nextNode = this.workspace.getNavigator().getNextSibling(node); - if (nextNode) return nextNode; - return this.findSiblingOrParentSibling( - this.workspace.getNavigator().getParent(node), - ); - } - /** * Get the right most child of a node. * @@ -466,17 +371,22 @@ export class LineCursor extends Marker { * @returns The right most child of the given node, or the node if no child * exists. */ - private getRightMostChild(node: INavigable): INavigable | null { + getRightMostChild( + node: INavigable | null, + stopIfFound: INavigable, + ): INavigable | null { + if (!node) return node; let newNode = this.workspace.getNavigator().getFirstChild(node); - if (!newNode) return node; + if (!newNode || newNode === stopIfFound) return node; for ( let nextNode: INavigable | null = newNode; nextNode; nextNode = this.workspace.getNavigator().getNextSibling(newNode) ) { + if (nextNode === stopIfFound) break; newNode = nextNode; } - return this.getRightMostChild(newNode); + return this.getRightMostChild(newNode, stopIfFound); } /** @@ -537,10 +447,7 @@ export class LineCursor extends Marker { this.potentialNodes = null; if (!nodes) throw new Error('must call preDelete first'); for (const node of nodes) { - if ( - this.validNode(node) && - !this.getSourceBlockFromNode(node)?.disposed - ) { + if (!this.getSourceBlockFromNode(node)?.disposed) { this.setCurNode(node); return; } diff --git a/core/keyboard_nav/workspace_navigation_policy.ts b/core/keyboard_nav/workspace_navigation_policy.ts index 656b0453b86..ed7dc5d0236 100644 --- a/core/keyboard_nav/workspace_navigation_policy.ts +++ b/core/keyboard_nav/workspace_navigation_policy.ts @@ -22,16 +22,7 @@ export class WorkspaceNavigationPolicy */ getFirstChild(current: WorkspaceSvg): INavigable | null { const blocks = current.getTopBlocks(true); - if (!blocks.length) return null; - const block = blocks[0]; - let topConnection = block.outputConnection; - if ( - !topConnection || - (block.previousConnection && block.previousConnection.isConnected()) - ) { - topConnection = block.previousConnection; - } - return topConnection ?? block; + return blocks.length ? blocks[0] : null; } /** diff --git a/tests/mocha/cursor_test.js b/tests/mocha/cursor_test.js index ded07a5617f..aa4f5618495 100644 --- a/tests/mocha/cursor_test.js +++ b/tests/mocha/cursor_test.js @@ -97,34 +97,34 @@ suite('Cursor', function () { this.cursor.setCurNode(prevNode); this.cursor.next(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode, this.blocks.B.getInput('NAME4').connection); + assert.equal(curNode, this.blocks.C); }); - test('In - From attached input connection', function () { + test('In - From field to attached input connection', function () { const fieldBlock = this.blocks.E; - const inputConnectionNode = this.blocks.A.inputList[0].connection; - this.cursor.setCurNode(inputConnectionNode); + const fieldNode = this.blocks.A.getField('NAME2'); + this.cursor.setCurNode(fieldNode); this.cursor.in(); const curNode = this.cursor.getCurNode(); assert.equal(curNode, fieldBlock); }); - test('Prev - From previous connection does not skip over next connection', function () { + test('Prev - From previous connection does skip over next connection', function () { const prevConnection = this.blocks.B.previousConnection; const prevConnectionNode = prevConnection; this.cursor.setCurNode(prevConnectionNode); this.cursor.prev(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode, this.blocks.A.nextConnection); + assert.equal(curNode, this.blocks.A); }); - test('Prev - From first connection loop to last next connection', function () { - const prevConnection = this.blocks.A.previousConnection; + test('Prev - From first block loop to last block', function () { + const prevConnection = this.blocks.A; const prevConnectionNode = prevConnection; this.cursor.setCurNode(prevConnectionNode); this.cursor.prev(); const curNode = this.cursor.getCurNode(); - assert.equal(curNode, this.blocks.D.nextConnection); + assert.equal(curNode, this.blocks.D); }); test('Out - From field does not skip over block node', function () { @@ -225,11 +225,11 @@ suite('Cursor', function () { }); test('getFirstNode', function () { const node = this.cursor.getFirstNode(); - assert.equal(node, this.blockA.previousConnection); + assert.equal(node, this.blockA); }); test('getLastNode', function () { const node = this.cursor.getLastNode(); - assert.equal(node, this.blockA.nextConnection); + assert.equal(node, this.blockA); }); }); @@ -242,11 +242,11 @@ suite('Cursor', function () { }); test('getFirstNode', function () { const node = this.cursor.getFirstNode(); - assert.equal(node, this.blockA.outputConnection); + assert.equal(node, this.blockA); }); test('getLastNode', function () { const node = this.cursor.getLastNode(); - assert.equal(node, this.blockA.inputList[0].connection); + assert.equal(node, this.blockA); }); }); suite('one c-hat block', function () { @@ -262,7 +262,7 @@ suite('Cursor', function () { }); test('getLastNode', function () { const node = this.cursor.getLastNode(); - assert.equal(node, this.blockA.inputList[0].connection); + assert.equal(node, this.blockA); }); }); @@ -295,12 +295,12 @@ suite('Cursor', function () { test('getFirstNode', function () { const node = this.cursor.getFirstNode(); const blockA = this.workspace.getBlockById('A'); - assert.equal(node, blockA.previousConnection); + assert.equal(node, blockA); }); test('getLastNode', function () { const node = this.cursor.getLastNode(); const blockB = this.workspace.getBlockById('B'); - assert.equal(node, blockB.nextConnection); + assert.equal(node, blockB); }); }); @@ -335,12 +335,12 @@ suite('Cursor', function () { test('getFirstNode', function () { const node = this.cursor.getFirstNode(); const blockA = this.workspace.getBlockById('A'); - assert.equal(node, blockA.outputConnection); + assert.equal(node, blockA); }); test('getLastNode', function () { const node = this.cursor.getLastNode(); const blockB = this.workspace.getBlockById('B'); - assert.equal(node, blockB.inputList[0].connection); + assert.equal(node, blockB); }); }); @@ -385,15 +385,14 @@ suite('Cursor', function () { test('getFirstNode', function () { const node = this.cursor.getFirstNode(); const location = node; - const previousConnection = - this.workspace.getBlockById('A').previousConnection; - assert.equal(location, previousConnection); + const blockA = this.workspace.getBlockById('A'); + assert.equal(location, blockA); }); test('getLastNode', function () { const node = this.cursor.getLastNode(); const location = node; - const nextConnection = this.workspace.getBlockById('D').nextConnection; - assert.equal(location, nextConnection); + const blockD = this.workspace.getBlockById('D'); + assert.equal(location, blockD); }); }); }); @@ -439,8 +438,8 @@ suite('Cursor', function () { this.cursor = this.workspace.getCursor(); this.neverValid = () => false; this.alwaysValid = () => true; - this.isConnection = (node) => { - return node && node instanceof Blockly.RenderedConnection; + this.isBlock = (node) => { + return node && node instanceof Blockly.BlockSvg; }; }); teardown(function () { @@ -528,7 +527,7 @@ suite('Cursor', function () { assert.equal(nextNode, this.blockB.getField('FIELD')); }); test('Always valid - start at end', function () { - const startNode = this.blockC.nextConnection; + const startNode = this.blockC.getField('FIELD'); const nextNode = this.cursor.getNextNode( startNode, this.alwaysValid, @@ -537,29 +536,29 @@ suite('Cursor', function () { assert.isNull(nextNode); }); - test('Valid if connection - start at top', function () { - const startNode = this.blockA.previousConnection; + test('Valid if block - start at top', function () { + const startNode = this.blockA; const nextNode = this.cursor.getNextNode( startNode, - this.isConnection, + this.isBlock, false, ); - assert.equal(nextNode, this.blockA.nextConnection); + assert.equal(nextNode, this.blockB); }); - test('Valid if connection - start in middle', function () { + test('Valid if block - start in middle', function () { const startNode = this.blockB; const nextNode = this.cursor.getNextNode( startNode, - this.isConnection, + this.isBlock, false, ); - assert.equal(nextNode, this.blockB.nextConnection); + assert.equal(nextNode, this.blockC); }); - test('Valid if connection - start at end', function () { - const startNode = this.blockC.nextConnection; + test('Valid if block - start at end', function () { + const startNode = this.blockC.getField('FIELD'); const nextNode = this.cursor.getNextNode( startNode, - this.isConnection, + this.isBlock, false, ); assert.isNull(nextNode); @@ -583,14 +582,10 @@ suite('Cursor', function () { assert.equal(nextNode, this.blockA.previousConnection); }); - test('Valid if connection - start at end - with loopback', function () { - const startNode = this.blockC.nextConnection; - const nextNode = this.cursor.getNextNode( - startNode, - this.isConnection, - true, - ); - assert.equal(nextNode, this.blockA.previousConnection); + test('Valid if block - start at end - with loopback', function () { + const startNode = this.blockC; + const nextNode = this.cursor.getNextNode(startNode, this.isBlock, true); + assert.equal(nextNode, this.blockA); }); }); }); @@ -637,8 +632,8 @@ suite('Cursor', function () { this.cursor = this.workspace.getCursor(); this.neverValid = () => false; this.alwaysValid = () => true; - this.isConnection = (node) => { - return node && node instanceof Blockly.RenderedConnection; + this.isBlock = (node) => { + return node && node instanceof Blockly.BlockSvg; }; }); teardown(function () { @@ -708,7 +703,7 @@ suite('Cursor', function () { }); test('Always valid - start at top', function () { - const startNode = this.blockA.previousConnection; + const startNode = this.blockA; const previousNode = this.cursor.getPreviousNode( startNode, this.alwaysValid, @@ -723,7 +718,7 @@ suite('Cursor', function () { this.alwaysValid, false, ); - assert.equal(previousNode, this.blockB.previousConnection); + assert.equal(previousNode, this.blockA.getField('FIELD')); }); test('Always valid - start at end', function () { const startNode = this.blockC.nextConnection; @@ -735,32 +730,32 @@ suite('Cursor', function () { assert.equal(previousNode, this.blockC.getField('FIELD')); }); - test('Valid if connection - start at top', function () { - const startNode = this.blockA.previousConnection; + test('Valid if block - start at top', function () { + const startNode = this.blockA; const previousNode = this.cursor.getPreviousNode( startNode, - this.isConnection, + this.isBlock, false, ); assert.isNull(previousNode); }); - test('Valid if connection - start in middle', function () { + test('Valid if block - start in middle', function () { const startNode = this.blockB; const previousNode = this.cursor.getPreviousNode( startNode, - this.isConnection, + this.isBlock, false, ); - assert.equal(previousNode, this.blockB.previousConnection); + assert.equal(previousNode, this.blockA); }); - test('Valid if connection - start at end', function () { - const startNode = this.blockC.nextConnection; + test('Valid if block - start at end', function () { + const startNode = this.blockC; const previousNode = this.cursor.getPreviousNode( startNode, - this.isConnection, + this.isBlock, false, ); - assert.equal(previousNode, this.blockC.previousConnection); + assert.equal(previousNode, this.blockB); }); test('Never valid - start at top - with loopback', function () { const startNode = this.blockA.previousConnection; @@ -780,14 +775,14 @@ suite('Cursor', function () { ); assert.equal(previousNode, this.blockC.nextConnection); }); - test('Valid if connection - start at top - with loopback', function () { - const startNode = this.blockA.previousConnection; + test('Valid if block - start at top - with loopback', function () { + const startNode = this.blockA; const previousNode = this.cursor.getPreviousNode( startNode, - this.isConnection, + this.isBlock, true, ); - assert.equal(previousNode, this.blockC.nextConnection); + assert.equal(previousNode, this.blockC); }); }); }); diff --git a/tests/mocha/navigation_test.js b/tests/mocha/navigation_test.js index b049170b079..0462d4daa0d 100644 --- a/tests/mocha/navigation_test.js +++ b/tests/mocha/navigation_test.js @@ -166,7 +166,7 @@ suite('Navigation', function () { }, { 'type': 'fields_and_input2', - 'message0': '%1 %2 %3 hi %4 bye', + 'message0': '%1 %2 %3 %4 bye', 'args0': [ { 'type': 'input_value', @@ -245,6 +245,7 @@ suite('Navigation', function () { const outputNextBlock = this.workspace.newBlock('output_next'); this.blocks.secondBlock = secondBlock; this.blocks.outputNextBlock = outputNextBlock; + this.workspace.cleanUp(); }); suite('Next', function () { setup(function () { @@ -261,12 +262,11 @@ suite('Navigation', function () { const nextNode = this.navigator.getNextSibling(prevConnection); assert.equal(nextNode, this.blocks.statementInput1); }); - test('fromBlockToNext', function () { - const nextConnection = this.blocks.statementInput1.nextConnection; + test('fromBlockToNextBlock', function () { const nextNode = this.navigator.getNextSibling( this.blocks.statementInput1, ); - assert.equal(nextNode, nextConnection); + assert.equal(nextNode, this.blocks.statementInput2); }); test('fromNextToPrevious', function () { const nextConnection = this.blocks.statementInput1.nextConnection; @@ -304,12 +304,12 @@ suite('Navigation', function () { const nextNode = this.navigator.getNextSibling(output); assert.equal(nextNode, this.blocks.fieldWithOutput); }); - test('fromFieldToInput', function () { + test('fromFieldToNestedBlock', function () { const field = this.blocks.statementInput1.inputList[0].fieldRow[1]; const inputConnection = this.blocks.statementInput1.inputList[0].connection; const nextNode = this.navigator.getNextSibling(field); - assert.equal(nextNode, inputConnection); + assert.equal(nextNode, this.blocks.fieldWithOutput); }); test('fromFieldToField', function () { const field = this.blocks.fieldAndInputs.inputList[0].fieldRow[0]; @@ -338,17 +338,17 @@ suite('Navigation', function () { }); test('fromBlockToPrevious', function () { const prevNode = this.navigator.getPreviousSibling( - this.blocks.statementInput1, + this.blocks.statementInput2, ); - const prevConnection = this.blocks.statementInput1.previousConnection; - assert.equal(prevNode, prevConnection); + const previousBlock = this.blocks.statementInput1; + assert.equal(prevNode, previousBlock); }); - test('fromBlockToOutput', function () { + test('fromOutputBlockToPreviousField', function () { const prevNode = this.navigator.getPreviousSibling( this.blocks.fieldWithOutput, ); const outputConnection = this.blocks.fieldWithOutput.outputConnection; - assert.equal(prevNode, outputConnection); + assert.equal(prevNode, [...this.blocks.statementInput1.getFields()][1]); }); test('fromNextToBlock', function () { const nextConnection = this.blocks.statementInput1.nextConnection; @@ -383,11 +383,16 @@ suite('Navigation', function () { assert.isNull(prevNode); }); test('fromFieldToInput', function () { + const outputBlock = this.workspace.newBlock('field_input'); + this.blocks.fieldAndInputs2.inputList[0].connection.connect( + outputBlock.outputConnection, + ); + const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0]; const inputConnection = this.blocks.fieldAndInputs2.inputList[0].connection; const prevNode = this.navigator.getPreviousSibling(field); - assert.equal(prevNode, inputConnection); + assert.equal(prevNode, outputBlock); }); test('fromFieldToField', function () { const field = this.blocks.fieldAndInputs.inputList[1].fieldRow[0]; @@ -423,10 +428,10 @@ suite('Navigation', function () { const inNode = this.navigator.getFirstChild(input.connection); assert.equal(inNode, previousConnection); }); - test('fromBlockToInput', function () { - const input = this.blocks.valueInput.inputList[0]; + test('fromBlockToField', function () { + const field = this.blocks.valueInput.getField('NAME'); const inNode = this.navigator.getFirstChild(this.blocks.valueInput); - assert.equal(inNode, input.connection); + assert.equal(inNode, field); }); test('fromBlockToField', function () { const inNode = this.navigator.getFirstChild( @@ -440,12 +445,10 @@ suite('Navigation', function () { assert.isNull(inNode); }); test('fromBlockToInput_DummyInputValue', function () { - const inputConnection = - this.blocks.dummyInputValue.inputList[1].connection; const inNode = this.navigator.getFirstChild( this.blocks.dummyInputValue, ); - assert.equal(inNode, inputConnection); + assert.equal(inNode, null); }); test('fromOuputToNull', function () { const output = this.blocks.fieldWithOutput.outputConnection; @@ -540,25 +543,25 @@ suite('Navigation', function () { const outNode = this.navigator.getParent(next); assert.equal(outNode, this.blocks.secondBlock.inputList[0].connection); }); - test('fromBlockToStack', function () { + test('fromBlockToWorkspace', function () { const outNode = this.navigator.getParent(this.blocks.statementInput2); - assert.equal(outNode, this.blocks.statementInput1); + assert.equal(outNode, this.workspace); }); - test('fromBlockToInput', function () { - const input = this.blocks.statementInput2.inputList[1].connection; + test('fromBlockToEnclosingStatement', function () { + const enclosingStatement = this.blocks.statementInput2; const outNode = this.navigator.getParent(this.blocks.statementInput3); - assert.equal(outNode, input); + assert.equal(outNode, enclosingStatement); }); - test('fromTopBlockToStack', function () { + test('fromTopBlockToWorkspace', function () { const outNode = this.navigator.getParent(this.blocks.statementInput1); - assert.equal(outNode, this.blocks.statementInput1); + assert.equal(outNode, this.workspace); }); - test('fromBlockToStack_OutputConnection', function () { + test('fromOutputBlockToWorkspace', function () { const outNode = this.navigator.getParent(this.blocks.fieldWithOutput2); - assert.equal(outNode, this.blocks.fieldWithOutput2); + assert.equal(outNode, this.workspace); }); - test('fromBlockToInput_OutputConnection', function () { - const inputConnection = this.blocks.secondBlock.inputList[0].connection; + test('fromOutputNextBlockToWorkspace', function () { + const inputConnection = this.blocks.secondBlock; const outNode = this.navigator.getParent(this.blocks.outputNextBlock); assert.equal(outNode, inputConnection); }); From 6bee1ca19685004051f9cf6a6e089b64d841e7e4 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Tue, 13 May 2025 11:45:21 -0700 Subject: [PATCH 204/222] chore: add node test for json with a dropdown field (#9019) --- tests/node/run_node_test.mjs | 112 +++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/tests/node/run_node_test.mjs b/tests/node/run_node_test.mjs index ee95fd5282f..f32286bbafb 100644 --- a/tests/node/run_node_test.mjs +++ b/tests/node/run_node_test.mjs @@ -37,6 +37,106 @@ const xmlText = ' \n' + ''; +const json = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'procedures_defnoreturn', + 'id': '0!;|f{%4H@mgQ`SIEDKV', + 'x': 38, + 'y': 163, + 'icons': { + 'comment': { + 'text': 'Describe this function...', + 'pinned': false, + 'height': 80, + 'width': 160, + }, + }, + 'fields': { + 'NAME': 'say hello', + }, + 'inputs': { + 'STACK': { + 'block': { + 'type': 'text_print', + 'id': 't^`WoL~R$t}rk]`JVFUP', + 'inputs': { + 'TEXT': { + 'shadow': { + 'type': 'text', + 'id': '_PxHV1tqEy60kP^].Qhh', + 'fields': { + 'TEXT': 'abc', + }, + }, + 'block': { + 'type': 'text_join', + 'id': 'K4.OZ9ql9j0f367238R@', + 'extraState': { + 'itemCount': 2, + }, + 'inputs': { + 'ADD0': { + 'block': { + 'type': 'text', + 'id': '5ElufS^j4;l:9N#|Yt$X', + 'fields': { + 'TEXT': 'The meaning of life is', + }, + }, + }, + 'ADD1': { + 'block': { + 'type': 'math_arithmetic', + 'id': ',QfcN`h]rQ86a]6J|di1', + 'fields': { + 'OP': 'MINUS', + }, + 'inputs': { + 'A': { + 'shadow': { + 'type': 'math_number', + 'id': 'ClcKUIPYleVQ_j7ZjK]^', + 'fields': { + 'NUM': 44, + }, + }, + }, + 'B': { + 'shadow': { + 'type': 'math_number', + 'id': 'F_cU|uaP7oB-k(j~@X?g', + 'fields': { + 'NUM': 2, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + 'type': 'procedures_callnoreturn', + 'id': 'Ad^$sruQ.`6zNmQ6jPit', + 'x': 38, + 'y': 113, + 'extraState': { + 'name': 'say hello', + }, + }, + ], + }, +}; + suite('Test Node.js', function () { test('Import XML', function () { const xml = Blockly.utils.xml.textToDom(xmlText); @@ -69,4 +169,16 @@ suite('Test Node.js', function () { // Check output assert.equal("window.alert('Hello from Blockly!');", code.trim(), 'equal'); }); + test('Import JSON', function () { + const workspace = new Blockly.Workspace(); + Blockly.serialization.workspaces.load(json, workspace); + }); + test('Roundtrip JSON', function () { + const workspace = new Blockly.Workspace(); + Blockly.serialization.workspaces.load(json, workspace); + + const jsonAfter = Blockly.serialization.workspaces.save(workspace); + + assert.deepEqual(jsonAfter, json); + }); }); From 14e1ef6dc625a8d73a6a9a50469a289fdeb4e4c7 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 13 May 2025 14:26:00 -0700 Subject: [PATCH 205/222] fix: Fix regressions in `Field`. (#9011) --- core/field.ts | 28 +++++++++++++++++++++++++++- core/field_image.ts | 1 - core/field_input.ts | 20 ++++++++++++++++++++ core/field_variable.ts | 1 - 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/core/field.ts b/core/field.ts index 7aa348d52c3..5e9092f88ad 100644 --- a/core/field.ts +++ b/core/field.ts @@ -84,6 +84,9 @@ export abstract class Field */ DEFAULT_VALUE: T | null = null; + /** Non-breaking space. */ + static readonly NBSP = '\u00A0'; + /** * A value used to signal when a field's constructor should *not* set the * field's value or run configure_, and should allow a subclass to do that @@ -106,7 +109,28 @@ export abstract class Field * field is not yet initialized. Is *not* guaranteed to be accurate. */ private tooltip: Tooltip.TipInfo | null = null; - protected size_: Size; + + /** This field's dimensions. */ + private size: Size = new Size(0, 0); + + /** + * Gets the size of this field. Because getSize() and updateSize() have side + * effects, this acts as a shim for subclasses which wish to adjust field + * bounds when setting/getting the size without triggering unwanted rendering + * or other side effects. Note that subclasses must override *both* get and + * set if either is overridden; the implementation may just call directly + * through to super, but it must exist per the JS spec. + */ + protected get size_(): Size { + return this.size; + } + + /** + * Sets the size of this field. + */ + protected set size_(newValue: Size) { + this.size = newValue; + } /** The rendered field's SVG group element. */ protected fieldGroup_: SVGGElement | null = null; @@ -969,6 +993,8 @@ export abstract class Field // Truncate displayed string and add an ellipsis ('...'). text = text.substring(0, this.maxDisplayLength - 2) + '…'; } + // Replace whitespace with non-breaking spaces so the text doesn't collapse. + text = text.replace(/\s/g, Field.NBSP); if (this.sourceBlock_ && this.sourceBlock_.RTL) { // The SVG is LTR, force text to be RTL by adding an RLM. text += '\u200F'; diff --git a/core/field_image.ts b/core/field_image.ts index b65b77ae29c..761294a8cde 100644 --- a/core/field_image.ts +++ b/core/field_image.ts @@ -27,7 +27,6 @@ export class FieldImage extends Field { * of the field. */ private static readonly Y_PADDING = 1; - protected override size_: Size; protected readonly imageHeight: number; /** The function to be called when this field is clicked. */ diff --git a/core/field_input.ts b/core/field_input.ts index 2cdd8056553..f329e0991dc 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -100,6 +100,26 @@ export abstract class FieldInput extends Field< */ override SERIALIZABLE = true; + /** + * Sets the size of this field. Although this appears to be a no-op, it must + * exist since the getter is overridden below. + */ + protected override set size_(newValue: Size) { + super.size_ = newValue; + } + + /** + * Returns the size of this field, with a minimum width of 14. + */ + protected override get size_() { + const s = super.size_; + if (s.width < 14) { + s.width = 14; + } + + return s; + } + /** * @param value The initial value of the field. Should cast to a string. * Defaults to an empty string if null or undefined. Also accepts diff --git a/core/field_variable.ts b/core/field_variable.ts index b54b113cd69..aa4fdfe310f 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -49,7 +49,6 @@ export class FieldVariable extends FieldDropdown { * dropdown. */ variableTypes: string[] | null = []; - protected override size_: Size; /** The variable model associated with this field. */ private variable: IVariableModel | null = null; From 556ee39f6f3ad9ed6b82b587ea85c539ff7a50b1 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Tue, 13 May 2025 14:30:28 -0700 Subject: [PATCH 206/222] fix!: remove deprecated setEnabled and backwards event filtering (#9039) --- core/block.ts | 42 --------------------------------------- core/block_svg.ts | 27 ------------------------- core/events/utils.ts | 18 ++--------------- tests/mocha/event_test.js | 35 ++++++++++---------------------- 4 files changed, 12 insertions(+), 110 deletions(-) diff --git a/core/block.ts b/core/block.ts index 132d19be495..43bc6bbc5ed 100644 --- a/core/block.ts +++ b/core/block.ts @@ -50,7 +50,6 @@ import * as registry from './registry.js'; import * as Tooltip from './tooltip.js'; import * as arrayUtils from './utils/array.js'; import {Coordinate} from './utils/coordinate.js'; -import * as deprecation from './utils/deprecation.js'; import * as idGenerator from './utils/idgenerator.js'; import * as parsing from './utils/parsing.js'; import {Size} from './utils/size.js'; @@ -1410,47 +1409,6 @@ export class Block { return this.disabledReasons.size === 0; } - /** @deprecated v11 - Get or sets whether the block is manually disabled. */ - private get disabled(): boolean { - deprecation.warn( - 'disabled', - 'v11', - 'v12', - 'the isEnabled or hasDisabledReason methods of Block', - ); - return this.hasDisabledReason(constants.MANUALLY_DISABLED); - } - - private set disabled(value: boolean) { - deprecation.warn( - 'disabled', - 'v11', - 'v12', - 'the setDisabledReason method of Block', - ); - this.setDisabledReason(value, constants.MANUALLY_DISABLED); - } - - /** - * @deprecated v11 - Set whether the block is manually enabled or disabled. - * The user can toggle whether a block is disabled from a context menu - * option. A block may still be disabled for other reasons even if the user - * attempts to manually enable it, such as when the block is in an invalid - * location. This method is deprecated and setDisabledReason should be used - * instead. - * - * @param enabled True if enabled. - */ - setEnabled(enabled: boolean) { - deprecation.warn( - 'setEnabled', - 'v11', - 'v12', - 'the setDisabledReason method of Block', - ); - this.setDisabledReason(!enabled, constants.MANUALLY_DISABLED); - } - /** * Add or remove a reason why the block might be disabled. If a block has * any reasons to be disabled, then the block itself will be considered diff --git a/core/block_svg.ts b/core/block_svg.ts index 1ec4d98eafe..26b8c4fb52f 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -57,7 +57,6 @@ import * as blocks from './serialization/blocks.js'; import type {BlockStyle} from './theme.js'; import * as Tooltip from './tooltip.js'; import {Coordinate} from './utils/coordinate.js'; -import * as deprecation from './utils/deprecation.js'; import * as dom from './utils/dom.js'; import {Rect} from './utils/rect.js'; import {Svg} from './utils/svg.js'; @@ -1078,32 +1077,6 @@ export class BlockSvg return removed; } - /** - * Set whether the block is manually enabled or disabled. - * - * The user can toggle whether a block is disabled from a context menu - * option. A block may still be disabled for other reasons even if the user - * attempts to manually enable it, such as when the block is in an invalid - * location. This method is deprecated and setDisabledReason should be used - * instead. - * - * @deprecated v11: use setDisabledReason. - * @param enabled True if enabled. - */ - override setEnabled(enabled: boolean) { - deprecation.warn( - 'setEnabled', - 'v11', - 'v12', - 'the setDisabledReason method of BlockSvg', - ); - const wasEnabled = this.isEnabled(); - super.setEnabled(enabled); - if (this.isEnabled() !== wasEnabled && !this.getInheritedDisabled()) { - this.updateDisabled(); - } - } - /** * Add or remove a reason why the block might be disabled. If a block has * any reasons to be disabled, then the block itself will be considered diff --git a/core/events/utils.ts b/core/events/utils.ts index 4753e7783d9..ac78c694273 100644 --- a/core/events/utils.ts +++ b/core/events/utils.ts @@ -9,7 +9,6 @@ import type {Block} from '../block.js'; import * as common from '../common.js'; import * as registry from '../registry.js'; -import * as deprecation from '../utils/deprecation.js'; import * as idGenerator from '../utils/idgenerator.js'; import type {Workspace} from '../workspace.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; @@ -124,7 +123,7 @@ function fireInternal(event: Abstract) { /** Dispatch all queued events. */ function fireNow() { - const queue = filter(FIRE_QUEUE, true); + const queue = filter(FIRE_QUEUE); FIRE_QUEUE.length = 0; for (const event of queue) { if (!event.workspaceId) continue; @@ -227,18 +226,9 @@ function enqueueEvent(event: Abstract) { * cause them to be reordered. * * @param queue Array of events. - * @param forward True if forward (redo), false if backward (undo). - * This parameter is deprecated: true is now the default and - * calling filter with it set to false will in future not be - * supported. * @returns Array of filtered events. */ -export function filter(queue: Abstract[], forward = true): Abstract[] { - if (!forward) { - deprecation.warn('filter(queue, /*forward=*/false)', 'v11.2', 'v12'); - // Undo was merged in reverse order. - queue = queue.slice().reverse(); // Copy before reversing in place. - } +export function filter(queue: Abstract[]): Abstract[] { const mergedQueue: Abstract[] = []; // Merge duplicates. for (const event of queue) { @@ -290,10 +280,6 @@ export function filter(queue: Abstract[], forward = true): Abstract[] { } // Filter out any events that have become null due to merging. queue = mergedQueue.filter((e) => !e.isNull()); - if (!forward) { - // Restore undo order. - queue.reverse(); - } return queue; } diff --git a/tests/mocha/event_test.js b/tests/mocha/event_test.js index ea58983c676..00d704ff052 100644 --- a/tests/mocha/event_test.js +++ b/tests/mocha/event_test.js @@ -1222,7 +1222,7 @@ suite('Events', function () { new Blockly.Events.BlockChange(block, 'field', 'VAR', 'id1', 'id2'), new Blockly.Events.Click(block), ]; - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); assert.equal(filteredEvents.length, 4); // no event should have been removed. // test that the order hasn't changed assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); @@ -1240,7 +1240,7 @@ suite('Events', function () { new Blockly.Events.BlockCreate(block2), new Blockly.Events.BlockMove(block2), ]; - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); assert.equal(filteredEvents.length, 4); // no event should have been removed. }); @@ -1250,7 +1250,7 @@ suite('Events', function () { addMoveEvent(events, block, 1, 1); addMoveEvent(events, block, 2, 2); addMoveEvent(events, block, 3, 3); - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); assert.equal(filteredEvents.length, 2); // duplicate moves should have been removed. // test that the order hasn't changed assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); @@ -1259,27 +1259,12 @@ suite('Events', function () { assert.equal(filteredEvents[1].newCoordinate.y, 3); }); - test('Backward', function () { - const block = this.workspace.newBlock('field_variable_test_block', '1'); - const events = [new Blockly.Events.BlockCreate(block)]; - addMoveEvent(events, block, 1, 1); - addMoveEvent(events, block, 2, 2); - addMoveEvent(events, block, 3, 3); - const filteredEvents = eventUtils.filter(events, false); - assert.equal(filteredEvents.length, 2); // duplicate event should have been removed. - // test that the order hasn't changed - assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); - assert.isTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); - assert.equal(filteredEvents[1].newCoordinate.x, 1); - assert.equal(filteredEvents[1].newCoordinate.y, 1); - }); - test('Merge block move events', function () { const block = this.workspace.newBlock('field_variable_test_block', '1'); const events = []; addMoveEvent(events, block, 0, 0); addMoveEvent(events, block, 1, 1); - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); assert.equal(filteredEvents.length, 1); // second move event merged into first assert.equal(filteredEvents[0].newCoordinate.x, 1); assert.equal(filteredEvents[0].newCoordinate.y, 1); @@ -1297,7 +1282,7 @@ suite('Events', function () { 'item2', ), ]; - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); assert.equal(filteredEvents.length, 1); // second change event merged into first assert.equal(filteredEvents[0].oldValue, 'item'); assert.equal(filteredEvents[0].newValue, 'item2'); @@ -1308,7 +1293,7 @@ suite('Events', function () { new Blockly.Events.ViewportChange(1, 2, 3, this.workspace, 4), new Blockly.Events.ViewportChange(5, 6, 7, this.workspace, 8), ]; - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); assert.equal(filteredEvents.length, 1); // second change event merged into first assert.equal(filteredEvents[0].viewTop, 5); assert.equal(filteredEvents[0].viewLeft, 6); @@ -1328,7 +1313,7 @@ suite('Events', function () { new Blockly.Events.BubbleOpen(block3, true, 'warning'), new Blockly.Events.Click(block3), ]; - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); // click event merged into corresponding *Open event assert.equal(filteredEvents.length, 3); assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BubbleOpen); @@ -1347,7 +1332,7 @@ suite('Events', function () { new Blockly.Events.Click(block), new Blockly.Events.BlockDrag(block, true), ]; - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); // click and stackclick should both exist assert.equal(filteredEvents.length, 2); assert.isTrue(filteredEvents[0] instanceof Blockly.Events.Click); @@ -1367,7 +1352,7 @@ suite('Events', function () { const events = []; addMoveEventParent(events, block, null); addMoveEventParent(events, block, null); - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); // The two events should be merged, but because nothing has changed // they will be filtered out. assert.equal(filteredEvents.length, 0); @@ -1388,7 +1373,7 @@ suite('Events', function () { events.push(new Blockly.Events.BlockDelete(block2)); addMoveEvent(events, block1, 2, 2); - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); // Nothing should have merged. assert.equal(filteredEvents.length, 4); // test that the order hasn't changed From e34a9690eddf4512f682aa80c697211123326a7c Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 13 May 2025 14:37:58 -0700 Subject: [PATCH 207/222] fix: Ensure selection stays when dragging blocks (#9034) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #9027 ### Proposed Changes Ensure that a block being dragged is properly focused mid-drag. ### Reason for Changes Focus seems to be lost due to the block being moved to the drag layer, so re-focusing the block ensures that it remains both actively focused and selected while dragging. The regression was likely caused when block selection was moved to be fully synced based on active focus. ### Test Coverage This has been manually verified in Core's simple playground. At the time of the PR being opened, this couldn't be tested in the test environment for the experimental keyboard navigation plugin since there's a navigation connection issue there that needs to be resolved to test movement. It would be helpful to add a new test case for the underlying problem (i.e. ensuring that the block holds focus mid-drag) as part of resolving #8915. ### Documentation No new documentation should need to be added. ### Additional Information This was found during the development of https://github.com/google/blockly-keyboard-experimentation/pull/511. --- core/layer_manager.ts | 14 ++++++++++++-- core/renderers/zelos/path_object.ts | 12 ++++++++++++ tests/mocha/layering_test.js | 9 +++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/core/layer_manager.ts b/core/layer_manager.ts index a7cb579348f..1d5afdd74e9 100644 --- a/core/layer_manager.ts +++ b/core/layer_manager.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {getFocusManager} from './focus_manager.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import {IRenderedElement} from './interfaces/i_rendered_element.js'; import * as layerNums from './layers.js'; import {Coordinate} from './utils/coordinate.js'; @@ -99,8 +101,12 @@ export class LayerManager { * * @internal */ - moveToDragLayer(elem: IRenderedElement) { + moveToDragLayer(elem: IRenderedElement & IFocusableNode) { this.dragLayer?.appendChild(elem.getSvgRoot()); + + // Since moving the element to the drag layer will cause it to lose focus, + // ensure it regains focus (to ensure proper highlights & sent events). + getFocusManager().focusNode(elem); } /** @@ -108,8 +114,12 @@ export class LayerManager { * * @internal */ - moveOffDragLayer(elem: IRenderedElement, layerNum: number) { + moveOffDragLayer(elem: IRenderedElement & IFocusableNode, layerNum: number) { this.append(elem, layerNum); + + // Since moving the element off the drag layer will cause it to lose focus, + // ensure it regains focus (to ensure proper highlights & sent events). + getFocusManager().focusNode(elem); } /** diff --git a/core/renderers/zelos/path_object.ts b/core/renderers/zelos/path_object.ts index f40426483a7..3c304fd6bf8 100644 --- a/core/renderers/zelos/path_object.ts +++ b/core/renderers/zelos/path_object.ts @@ -8,6 +8,7 @@ import type {BlockSvg} from '../../block_svg.js'; import type {Connection} from '../../connection.js'; +import {FocusManager} from '../../focus_manager.js'; import type {BlockStyle} from '../../theme.js'; import * as dom from '../../utils/dom.js'; import {Svg} from '../../utils/svg.js'; @@ -91,6 +92,17 @@ export class PathObject extends BasePathObject { if (!this.svgPathSelected) { this.svgPathSelected = this.svgPath.cloneNode(true) as SVGElement; this.svgPathSelected.classList.add('blocklyPathSelected'); + // Ensure focus-specific properties don't overlap with the block's path. + dom.removeClass( + this.svgPathSelected, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + dom.removeClass( + this.svgPathSelected, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + this.svgPathSelected.removeAttribute('tabindex'); + this.svgPathSelected.removeAttribute('id'); this.svgRoot.appendChild(this.svgPathSelected); } } else { diff --git a/tests/mocha/layering_test.js b/tests/mocha/layering_test.js index efc3ef3d632..1ef0ee6973d 100644 --- a/tests/mocha/layering_test.js +++ b/tests/mocha/layering_test.js @@ -24,6 +24,15 @@ suite('Layering', function () { const g = Blockly.utils.dom.createSvgElement('g', {}); return { getSvgRoot: () => g, + getFocusableElement: () => { + throw new Error('Unsupported.'); + }, + getFocusableTree: () => { + throw new Error('Unsupported.'); + }, + onNodeFocus: () => {}, + onNodeBlur: () => {}, + canBeFocused: () => false, }; } From ae22165cbe222823cd771ca4ff664404c400ba41 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 13 May 2025 15:04:49 -0700 Subject: [PATCH 208/222] refactor: Remove INavigable in favor of IFocusableNode. (#9037) * refactor: Remove INavigable in favor of IFocusableNode. * chore: Fix JSDoc. * chore: Address review feedback. --- core/block_svg.ts | 16 +- core/blockly.ts | 5 +- core/field.ts | 18 +- core/field_checkbox.ts | 12 -- core/field_dropdown.ts | 12 -- core/field_image.ts | 12 -- core/field_label.ts | 12 -- core/field_number.ts | 12 -- core/field_textinput.ts | 12 -- core/flyout_base.ts | 2 + core/flyout_button.ts | 19 +-- core/flyout_item.ts | 5 +- core/flyout_navigator.ts | 24 +++ core/flyout_separator.ts | 17 +- core/interfaces/i_navigable.ts | 19 --- core/interfaces/i_navigation_policy.ts | 19 ++- core/keyboard_nav/block_navigation_policy.ts | 22 ++- .../connection_navigation_policy.ts | 22 ++- core/keyboard_nav/field_navigation_policy.ts | 22 ++- .../flyout_button_navigation_policy.ts | 22 ++- core/keyboard_nav/flyout_navigation_policy.ts | 20 ++- .../flyout_separator_navigation_policy.ts | 22 ++- core/keyboard_nav/line_cursor.ts | 161 ++++-------------- core/keyboard_nav/marker.ts | 10 +- .../workspace_navigation_policy.ts | 22 ++- core/navigator.ts | 48 +++--- core/rendered_connection.ts | 12 +- core/workspace_svg.ts | 26 ++- .../src/field/different_user_input.ts | 4 - 29 files changed, 236 insertions(+), 393 deletions(-) create mode 100644 core/flyout_navigator.ts delete mode 100644 core/interfaces/i_navigable.ts diff --git a/core/block_svg.ts b/core/block_svg.ts index 26b8c4fb52f..ca25560fd01 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -47,7 +47,6 @@ import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {IIcon} from './interfaces/i_icon.js'; -import type {INavigable} from './interfaces/i_navigable.js'; import * as internalConstants from './internal_constants.js'; import {Msg} from './msg.js'; import * as renderManagement from './render_management.js'; @@ -77,8 +76,7 @@ export class BlockSvg ICopyable, IDraggable, IDeletable, - IFocusableNode, - INavigable + IFocusableNode { /** * Constant for identifying rows that are to be rendered inline. @@ -1796,16 +1794,4 @@ export class BlockSvg canBeFocused(): boolean { return true; } - - /** - * Returns this block's class. - * - * Used by keyboard navigation to look up the rules for navigating from this - * block. - * - * @returns This block's class. - */ - getClass() { - return BlockSvg; - } } diff --git a/core/blockly.ts b/core/blockly.ts index f7b44b2efa5..14383a947a3 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -171,7 +171,7 @@ import { import {IVariableMap} from './interfaces/i_variable_map.js'; import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import * as internalConstants from './internal_constants.js'; -import {CursorOptions, LineCursor} from './keyboard_nav/line_cursor.js'; +import {LineCursor} from './keyboard_nav/line_cursor.js'; import {Marker} from './keyboard_nav/marker.js'; import type {LayerManager} from './layer_manager.js'; import * as layers from './layers.js'; @@ -429,7 +429,7 @@ Names.prototype.populateProcedures = function ( }; // clang-format on -export * from './interfaces/i_navigable.js'; +export * from './flyout_navigator.js'; export * from './interfaces/i_navigation_policy.js'; export * from './keyboard_nav/block_navigation_policy.js'; export * from './keyboard_nav/connection_navigation_policy.js'; @@ -457,7 +457,6 @@ export { ContextMenuItems, ContextMenuRegistry, Css, - CursorOptions, DeleteArea, DragTarget, Events, diff --git a/core/field.ts b/core/field.ts index 5e9092f88ad..f7e01527e5d 100644 --- a/core/field.ts +++ b/core/field.ts @@ -26,7 +26,6 @@ import type {Input} from './inputs/input.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js'; -import type {INavigable} from './interfaces/i_navigable.js'; import type {IRegistrable} from './interfaces/i_registrable.js'; import {ISerializable} from './interfaces/i_serializable.js'; import type {ConstantProvider} from './renderers/common/constants.js'; @@ -68,12 +67,7 @@ export type FieldValidator = (newValue: T) => T | null | undefined; * @typeParam T - The value stored on the field. */ export abstract class Field - implements - IKeyboardAccessible, - IRegistrable, - ISerializable, - IFocusableNode, - INavigable> + implements IKeyboardAccessible, IRegistrable, ISerializable, IFocusableNode { /** * To overwrite the default value which is set in **Field**, directly update @@ -1410,16 +1404,6 @@ export abstract class Field `Attempted to instantiate a field from the registry that hasn't defined a 'fromJson' method.`, ); } - - /** - * Returns this field's class. - * - * Used by keyboard navigation to look up the rules for navigating from this - * field. Must be implemented by subclasses. - * - * @returns This field's class. - */ - abstract getClass(): new (...args: any) => Field; } /** diff --git a/core/field_checkbox.ts b/core/field_checkbox.ts index 85c176bc3ae..55ed42cbf4b 100644 --- a/core/field_checkbox.ts +++ b/core/field_checkbox.ts @@ -228,18 +228,6 @@ export class FieldCheckbox extends Field { // 'override' the static fromJson method. return new this(options.checked, undefined, options); } - - /** - * Returns this field's class. - * - * Used by keyboard navigation to look up the rules for navigating from this - * field. - * - * @returns This field's class. - */ - getClass() { - return FieldCheckbox; - } } fieldRegistry.register('field_checkbox', FieldCheckbox); diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 38c5727f2b6..0b787bd7701 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -796,18 +796,6 @@ export class FieldDropdown extends Field { throw TypeError('Found invalid FieldDropdown options.'); } } - - /** - * Returns this field's class. - * - * Used by keyboard navigation to look up the rules for navigating from this - * field. - * - * @returns This field's class. - */ - getClass() { - return FieldDropdown; - } } /** diff --git a/core/field_image.ts b/core/field_image.ts index 761294a8cde..6dfe2530e50 100644 --- a/core/field_image.ts +++ b/core/field_image.ts @@ -272,18 +272,6 @@ export class FieldImage extends Field { options, ); } - - /** - * Returns this field's class. - * - * Used by keyboard navigation to look up the rules for navigating from this - * field. - * - * @returns This field's class. - */ - getClass() { - return FieldImage; - } } fieldRegistry.register('field_image', FieldImage); diff --git a/core/field_label.ts b/core/field_label.ts index 6b8437d2b95..236154cc7b1 100644 --- a/core/field_label.ts +++ b/core/field_label.ts @@ -126,18 +126,6 @@ export class FieldLabel extends Field { // the static fromJson method. return new this(text, undefined, options); } - - /** - * Returns this field's class. - * - * Used by keyboard navigation to look up the rules for navigating from this - * field. - * - * @returns This field's class. - */ - getClass() { - return FieldLabel; - } } fieldRegistry.register('field_label', FieldLabel); diff --git a/core/field_number.ts b/core/field_number.ts index 6ecda1011be..7e36591753e 100644 --- a/core/field_number.ts +++ b/core/field_number.ts @@ -341,18 +341,6 @@ export class FieldNumber extends FieldInput { options, ); } - - /** - * Returns this field's class. - * - * Used by keyboard navigation to look up the rules for navigating from this - * field. - * - * @returns This field's class. - */ - getClass() { - return FieldNumber; - } } fieldRegistry.register('field_number', FieldNumber); diff --git a/core/field_textinput.ts b/core/field_textinput.ts index 6ec2331d023..2b896ad47be 100644 --- a/core/field_textinput.ts +++ b/core/field_textinput.ts @@ -89,18 +89,6 @@ export class FieldTextInput extends FieldInput { // override the static fromJson method. return new this(text, undefined, options); } - - /** - * Returns this field's class. - * - * Used by keyboard navigation to look up the rules for navigating from this - * field. - * - * @returns This field's class. - */ - getClass() { - return FieldTextInput; - } } fieldRegistry.register('field_input', FieldTextInput); diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 2af2808205c..9f94ec30905 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -20,6 +20,7 @@ import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import {FlyoutItem} from './flyout_item.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; +import {FlyoutNavigator} from './flyout_navigator.js'; import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; import {getFocusManager} from './focus_manager.js'; import {IAutoHideable} from './interfaces/i_autohideable.js'; @@ -243,6 +244,7 @@ export abstract class Flyout this.workspace_.internalIsFlyout = true; // Keep the workspace visibility consistent with the flyout's visibility. this.workspace_.setVisible(this.visible); + this.workspace_.setNavigator(new FlyoutNavigator(this)); /** * The unique id for this component that is used to register with the diff --git a/core/flyout_button.ts b/core/flyout_button.ts index 605a1358535..823b57be765 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -16,7 +16,6 @@ import * as Css from './css.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; -import type {INavigable} from './interfaces/i_navigable.js'; import type {IRenderedElement} from './interfaces/i_rendered_element.js'; import {idGenerator} from './utils.js'; import {Coordinate} from './utils/coordinate.js'; @@ -32,11 +31,7 @@ import type {WorkspaceSvg} from './workspace_svg.js'; * Class for a button or label in the flyout. */ export class FlyoutButton - implements - IBoundedElement, - IRenderedElement, - IFocusableNode, - INavigable + implements IBoundedElement, IRenderedElement, IFocusableNode { /** The horizontal margin around the text in the button. */ static TEXT_MARGIN_X = 5; @@ -412,18 +407,6 @@ export class FlyoutButton canBeFocused(): boolean { return true; } - - /** - * Returns this button's class. - * - * Used by keyboard navigation to look up the rules for navigating from this - * button. - * - * @returns This button's class. - */ - getClass() { - return FlyoutButton; - } } /** CSS for buttons and labels. See css.js for use. */ diff --git a/core/flyout_item.ts b/core/flyout_item.ts index ab973cb97e4..26be0ed12e2 100644 --- a/core/flyout_item.ts +++ b/core/flyout_item.ts @@ -1,5 +1,6 @@ import type {IBoundedElement} from './interfaces/i_bounded_element.js'; -import type {INavigable} from './interfaces/i_navigable.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; + /** * Representation of an item displayed in a flyout. */ @@ -12,7 +13,7 @@ export class FlyoutItem { * flyout inflater that created this object. */ constructor( - private element: IBoundedElement & INavigable, + private element: IBoundedElement & IFocusableNode, private type: string, ) {} diff --git a/core/flyout_navigator.ts b/core/flyout_navigator.ts new file mode 100644 index 00000000000..a102ce81765 --- /dev/null +++ b/core/flyout_navigator.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFlyout} from './interfaces/i_flyout.js'; +import {FlyoutButtonNavigationPolicy} from './keyboard_nav/flyout_button_navigation_policy.js'; +import {FlyoutNavigationPolicy} from './keyboard_nav/flyout_navigation_policy.js'; +import {FlyoutSeparatorNavigationPolicy} from './keyboard_nav/flyout_separator_navigation_policy.js'; +import {Navigator} from './navigator.js'; + +export class FlyoutNavigator extends Navigator { + constructor(flyout: IFlyout) { + super(); + this.rules.push( + new FlyoutButtonNavigationPolicy(), + new FlyoutSeparatorNavigationPolicy(), + ); + this.rules = this.rules.map( + (rule) => new FlyoutNavigationPolicy(rule, flyout), + ); + } +} diff --git a/core/flyout_separator.ts b/core/flyout_separator.ts index 73698b3dd6c..e9ace428ec9 100644 --- a/core/flyout_separator.ts +++ b/core/flyout_separator.ts @@ -7,15 +7,12 @@ import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; -import type {INavigable} from './interfaces/i_navigable.js'; import {Rect} from './utils/rect.js'; /** * Representation of a gap between elements in a flyout. */ -export class FlyoutSeparator - implements IBoundedElement, INavigable, IFocusableNode -{ +export class FlyoutSeparator implements IBoundedElement, IFocusableNode { private x = 0; private y = 0; @@ -66,18 +63,6 @@ export class FlyoutSeparator return false; } - /** - * Returns this separator's class. - * - * Used by keyboard navigation to look up the rules for navigating from this - * separator. - * - * @returns This separator's class. - */ - getClass() { - return FlyoutSeparator; - } - /** See IFocusableNode.getFocusableElement. */ getFocusableElement(): HTMLElement | SVGElement { throw new Error('Cannot be focused'); diff --git a/core/interfaces/i_navigable.ts b/core/interfaces/i_navigable.ts deleted file mode 100644 index dcb8ad50fca..00000000000 --- a/core/interfaces/i_navigable.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type {IFocusableNode} from './i_focusable_node.js'; - -/** - * Represents a UI element which can be navigated to using the keyboard. - */ -export interface INavigable extends IFocusableNode { - /** - * Returns the class of this instance. - * - * @returns This object's class. - */ - getClass(): new (...args: any) => T; -} diff --git a/core/interfaces/i_navigation_policy.ts b/core/interfaces/i_navigation_policy.ts index d2b694c897a..8e1ce6c1005 100644 --- a/core/interfaces/i_navigation_policy.ts +++ b/core/interfaces/i_navigation_policy.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {INavigable} from './i_navigable.js'; +import type {IFocusableNode} from './i_focusable_node.js'; /** * A set of rules that specify where keyboard navigation should proceed. @@ -16,7 +16,7 @@ export interface INavigationPolicy { * @param current The element which the user is navigating into. * @returns The current element's first child, or null if it has none. */ - getFirstChild(current: T): INavigable | null; + getFirstChild(current: T): IFocusableNode | null; /** * Returns the parent element of the given element, if any. @@ -24,7 +24,7 @@ export interface INavigationPolicy { * @param current The element which the user is navigating out of. * @returns The parent element of the current element, or null if it has none. */ - getParent(current: T): INavigable | null; + getParent(current: T): IFocusableNode | null; /** * Returns the peer element following the given element, if any. @@ -33,7 +33,7 @@ export interface INavigationPolicy { * @returns The next peer element of the current element, or null if there is * none. */ - getNextSibling(current: T): INavigable | null; + getNextSibling(current: T): IFocusableNode | null; /** * Returns the peer element preceding the given element, if any. @@ -42,7 +42,7 @@ export interface INavigationPolicy { * @returns The previous peer element of the current element, or null if * there is none. */ - getPreviousSibling(current: T): INavigable | null; + getPreviousSibling(current: T): IFocusableNode | null; /** * Returns whether or not the given instance should be reachable via keyboard @@ -57,4 +57,13 @@ export interface INavigationPolicy { * @returns True if this element should be included in keyboard navigation. */ isNavigable(current: T): boolean; + + /** + * Returns whether or not this navigation policy corresponds to the type of + * the given object. + * + * @param current An instance to check whether this policy applies to. + * @returns True if the given object is of a type handled by this policy. + */ + isApplicable(current: any): current is T; } diff --git a/core/keyboard_nav/block_navigation_policy.ts b/core/keyboard_nav/block_navigation_policy.ts index 55c7086ec9f..74f970d9961 100644 --- a/core/keyboard_nav/block_navigation_policy.ts +++ b/core/keyboard_nav/block_navigation_policy.ts @@ -6,7 +6,7 @@ import {BlockSvg} from '../block_svg.js'; import type {Field} from '../field.js'; -import type {INavigable} from '../interfaces/i_navigable.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; import {WorkspaceSvg} from '../workspace_svg.js'; @@ -20,7 +20,7 @@ export class BlockNavigationPolicy implements INavigationPolicy { * @param current The block to return the first child of. * @returns The first field or input of the given block, if any. */ - getFirstChild(current: BlockSvg): INavigable | null { + getFirstChild(current: BlockSvg): IFocusableNode | null { for (const input of current.inputList) { for (const field of input.fieldRow) { return field; @@ -39,7 +39,7 @@ export class BlockNavigationPolicy implements INavigationPolicy { * @returns The top block of the given block's stack, or the connection to * which it is attached. */ - getParent(current: BlockSvg): INavigable | null { + getParent(current: BlockSvg): IFocusableNode | null { if (current.previousConnection?.targetBlock()) { const surroundParent = current.getSurroundParent(); if (surroundParent) return surroundParent; @@ -57,7 +57,7 @@ export class BlockNavigationPolicy implements INavigationPolicy { * @returns The first block of the next stack if the given block is a terminal * block, or its next connection. */ - getNextSibling(current: BlockSvg): INavigable | null { + getNextSibling(current: BlockSvg): IFocusableNode | null { if (current.nextConnection?.targetBlock()) { return current.nextConnection?.targetBlock(); } @@ -101,7 +101,7 @@ export class BlockNavigationPolicy implements INavigationPolicy { * @returns The block's previous/output connection, or the last * connection/block of the previous block stack if it is a root block. */ - getPreviousSibling(current: BlockSvg): INavigable | null { + getPreviousSibling(current: BlockSvg): IFocusableNode | null { if (current.previousConnection?.targetBlock()) { return current.previousConnection?.targetBlock(); } @@ -127,7 +127,7 @@ export class BlockNavigationPolicy implements INavigationPolicy { } const currentIndex = siblings.indexOf(current); - let result: INavigable | null = null; + let result: IFocusableNode | null = null; if (currentIndex >= 1) { result = siblings[currentIndex - 1]; } else if (currentIndex === 0 && navigatingCrossStacks) { @@ -152,4 +152,14 @@ export class BlockNavigationPolicy implements INavigationPolicy { isNavigable(current: BlockSvg): boolean { return current.canBeFocused(); } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a BlockSvg. + */ + isApplicable(current: any): current is BlockSvg { + return current instanceof BlockSvg; + } } diff --git a/core/keyboard_nav/connection_navigation_policy.ts b/core/keyboard_nav/connection_navigation_policy.ts index 8323a73e148..9c3eafc56b0 100644 --- a/core/keyboard_nav/connection_navigation_policy.ts +++ b/core/keyboard_nav/connection_navigation_policy.ts @@ -6,9 +6,9 @@ import type {BlockSvg} from '../block_svg.js'; import {ConnectionType} from '../connection_type.js'; -import type {INavigable} from '../interfaces/i_navigable.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; -import type {RenderedConnection} from '../rendered_connection.js'; +import {RenderedConnection} from '../rendered_connection.js'; /** * Set of rules controlling keyboard navigation from a connection. @@ -22,7 +22,7 @@ export class ConnectionNavigationPolicy * @param current The connection to return the first child of. * @returns The connection's first child element, or null if not none. */ - getFirstChild(current: RenderedConnection): INavigable | null { + getFirstChild(current: RenderedConnection): IFocusableNode | null { if (current.getParentInput()) { return current.targetConnection; } @@ -36,7 +36,7 @@ export class ConnectionNavigationPolicy * @param current The connection to return the parent of. * @returns The given connection's parent connection or block. */ - getParent(current: RenderedConnection): INavigable | null { + getParent(current: RenderedConnection): IFocusableNode | null { if (current.type === ConnectionType.OUTPUT_VALUE) { return current.targetConnection ?? current.getSourceBlock(); } else if (current.getParentInput()) { @@ -56,7 +56,7 @@ export class ConnectionNavigationPolicy * @param current The connection to navigate from. * @returns The field, input connection or block following this connection. */ - getNextSibling(current: RenderedConnection): INavigable | null { + getNextSibling(current: RenderedConnection): IFocusableNode | null { if (current.getParentInput()) { const parentInput = current.getParentInput(); const block = parentInput?.getSourceBlock(); @@ -101,7 +101,7 @@ export class ConnectionNavigationPolicy * @param current The connection to navigate from. * @returns The field, input connection or block preceding this connection. */ - getPreviousSibling(current: RenderedConnection): INavigable | null { + getPreviousSibling(current: RenderedConnection): IFocusableNode | null { if (current.getParentInput()) { const parentInput = current.getParentInput(); const block = parentInput?.getSourceBlock(); @@ -176,4 +176,14 @@ export class ConnectionNavigationPolicy isNavigable(current: RenderedConnection): boolean { return current.canBeFocused(); } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a RenderedConnection. + */ + isApplicable(current: any): current is RenderedConnection { + return current instanceof RenderedConnection; + } } diff --git a/core/keyboard_nav/field_navigation_policy.ts b/core/keyboard_nav/field_navigation_policy.ts index a4a48df503a..9139b64aeed 100644 --- a/core/keyboard_nav/field_navigation_policy.ts +++ b/core/keyboard_nav/field_navigation_policy.ts @@ -5,8 +5,8 @@ */ import type {BlockSvg} from '../block_svg.js'; -import type {Field} from '../field.js'; -import type {INavigable} from '../interfaces/i_navigable.js'; +import {Field} from '../field.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; /** @@ -19,7 +19,7 @@ export class FieldNavigationPolicy implements INavigationPolicy> { * @param _current The field to navigate from. * @returns Null. */ - getFirstChild(_current: Field): INavigable | null { + getFirstChild(_current: Field): IFocusableNode | null { return null; } @@ -29,7 +29,7 @@ export class FieldNavigationPolicy implements INavigationPolicy> { * @param current The field to navigate from. * @returns The given field's parent block. */ - getParent(current: Field): INavigable | null { + getParent(current: Field): IFocusableNode | null { return current.getSourceBlock() as BlockSvg; } @@ -39,7 +39,7 @@ export class FieldNavigationPolicy implements INavigationPolicy> { * @param current The field to navigate from. * @returns The next field or input in the given field's block. */ - getNextSibling(current: Field): INavigable | null { + getNextSibling(current: Field): IFocusableNode | null { const input = current.getParentInput(); const block = current.getSourceBlock(); if (!block) return null; @@ -64,7 +64,7 @@ export class FieldNavigationPolicy implements INavigationPolicy> { * @param current The field to navigate from. * @returns The preceding field or input in the given field's block. */ - getPreviousSibling(current: Field): INavigable | null { + getPreviousSibling(current: Field): IFocusableNode | null { const parentInput = current.getParentInput(); const block = current.getSourceBlock(); if (!block) return null; @@ -106,4 +106,14 @@ export class FieldNavigationPolicy implements INavigationPolicy> { current.getParentInput().isVisible() ); } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a Field. + */ + isApplicable(current: any): current is Field { + return current instanceof Field; + } } diff --git a/core/keyboard_nav/flyout_button_navigation_policy.ts b/core/keyboard_nav/flyout_button_navigation_policy.ts index 771a327b589..6c39c3061e7 100644 --- a/core/keyboard_nav/flyout_button_navigation_policy.ts +++ b/core/keyboard_nav/flyout_button_navigation_policy.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {FlyoutButton} from '../flyout_button.js'; -import type {INavigable} from '../interfaces/i_navigable.js'; +import {FlyoutButton} from '../flyout_button.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; /** @@ -20,7 +20,7 @@ export class FlyoutButtonNavigationPolicy * @param _current The FlyoutButton instance to navigate from. * @returns Null. */ - getFirstChild(_current: FlyoutButton): INavigable | null { + getFirstChild(_current: FlyoutButton): IFocusableNode | null { return null; } @@ -30,7 +30,7 @@ export class FlyoutButtonNavigationPolicy * @param current The FlyoutButton instance to navigate from. * @returns The given flyout button's parent workspace. */ - getParent(current: FlyoutButton): INavigable | null { + getParent(current: FlyoutButton): IFocusableNode | null { return current.getWorkspace(); } @@ -40,7 +40,7 @@ export class FlyoutButtonNavigationPolicy * @param _current The FlyoutButton instance to navigate from. * @returns Null. */ - getNextSibling(_current: FlyoutButton): INavigable | null { + getNextSibling(_current: FlyoutButton): IFocusableNode | null { return null; } @@ -50,7 +50,7 @@ export class FlyoutButtonNavigationPolicy * @param _current The FlyoutButton instance to navigate from. * @returns Null. */ - getPreviousSibling(_current: FlyoutButton): INavigable | null { + getPreviousSibling(_current: FlyoutButton): IFocusableNode | null { return null; } @@ -63,4 +63,14 @@ export class FlyoutButtonNavigationPolicy isNavigable(current: FlyoutButton): boolean { return current.canBeFocused(); } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a FlyoutButton. + */ + isApplicable(current: any): current is FlyoutButton { + return current instanceof FlyoutButton; + } } diff --git a/core/keyboard_nav/flyout_navigation_policy.ts b/core/keyboard_nav/flyout_navigation_policy.ts index 1abdbfb6cd8..6552c27b499 100644 --- a/core/keyboard_nav/flyout_navigation_policy.ts +++ b/core/keyboard_nav/flyout_navigation_policy.ts @@ -5,7 +5,7 @@ */ import type {IFlyout} from '../interfaces/i_flyout.js'; -import type {INavigable} from '../interfaces/i_navigable.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; /** @@ -29,7 +29,7 @@ export class FlyoutNavigationPolicy implements INavigationPolicy { * @param _current The flyout item to navigate from. * @returns Null to prevent navigating into flyout items. */ - getFirstChild(_current: T): INavigable | null { + getFirstChild(_current: T): IFocusableNode | null { return null; } @@ -39,7 +39,7 @@ export class FlyoutNavigationPolicy implements INavigationPolicy { * @param current The flyout item to navigate from. * @returns The parent of the given flyout item. */ - getParent(current: T): INavigable | null { + getParent(current: T): IFocusableNode | null { return this.policy.getParent(current); } @@ -49,7 +49,7 @@ export class FlyoutNavigationPolicy implements INavigationPolicy { * @param current The flyout item to navigate from. * @returns The flyout item following the given one. */ - getNextSibling(current: T): INavigable | null { + getNextSibling(current: T): IFocusableNode | null { const flyoutContents = this.flyout.getContents(); if (!flyoutContents) return null; @@ -72,7 +72,7 @@ export class FlyoutNavigationPolicy implements INavigationPolicy { * @param current The flyout item to navigate from. * @returns The flyout item preceding the given one. */ - getPreviousSibling(current: T): INavigable | null { + getPreviousSibling(current: T): IFocusableNode | null { const flyoutContents = this.flyout.getContents(); if (!flyoutContents) return null; @@ -98,4 +98,14 @@ export class FlyoutNavigationPolicy implements INavigationPolicy { isNavigable(current: T): boolean { return this.policy.isNavigable(current); } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a BlockSvg. + */ + isApplicable(current: any): current is T { + return this.policy.isApplicable(current); + } } diff --git a/core/keyboard_nav/flyout_separator_navigation_policy.ts b/core/keyboard_nav/flyout_separator_navigation_policy.ts index 2e69af37a66..eb7ca4eb783 100644 --- a/core/keyboard_nav/flyout_separator_navigation_policy.ts +++ b/core/keyboard_nav/flyout_separator_navigation_policy.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {FlyoutSeparator} from '../flyout_separator.js'; -import type {INavigable} from '../interfaces/i_navigable.js'; +import {FlyoutSeparator} from '../flyout_separator.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; /** @@ -15,19 +15,19 @@ import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; export class FlyoutSeparatorNavigationPolicy implements INavigationPolicy { - getFirstChild(_current: FlyoutSeparator): INavigable | null { + getFirstChild(_current: FlyoutSeparator): IFocusableNode | null { return null; } - getParent(_current: FlyoutSeparator): INavigable | null { + getParent(_current: FlyoutSeparator): IFocusableNode | null { return null; } - getNextSibling(_current: FlyoutSeparator): INavigable | null { + getNextSibling(_current: FlyoutSeparator): IFocusableNode | null { return null; } - getPreviousSibling(_current: FlyoutSeparator): INavigable | null { + getPreviousSibling(_current: FlyoutSeparator): IFocusableNode | null { return null; } @@ -40,4 +40,14 @@ export class FlyoutSeparatorNavigationPolicy isNavigable(_current: FlyoutSeparator): boolean { return false; } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a FlyoutSeparator. + */ + isApplicable(current: any): current is FlyoutSeparator { + return current instanceof FlyoutSeparator; + } } diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 845fab9519c..71c61b4f3e4 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -14,43 +14,12 @@ */ import {BlockSvg} from '../block_svg.js'; -import {FieldCheckbox} from '../field_checkbox.js'; -import {FieldDropdown} from '../field_dropdown.js'; -import {FieldImage} from '../field_image.js'; -import {FieldLabel} from '../field_label.js'; -import {FieldNumber} from '../field_number.js'; -import {FieldTextInput} from '../field_textinput.js'; -import {FlyoutButton} from '../flyout_button.js'; -import {FlyoutSeparator} from '../flyout_separator.js'; import {getFocusManager} from '../focus_manager.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import {isFocusableNode} from '../interfaces/i_focusable_node.js'; -import type {INavigable} from '../interfaces/i_navigable.js'; import * as registry from '../registry.js'; -import {RenderedConnection} from '../rendered_connection.js'; -import {Renderer} from '../renderers/zelos/renderer.js'; import {WorkspaceSvg} from '../workspace_svg.js'; -import {BlockNavigationPolicy} from './block_navigation_policy.js'; -import {ConnectionNavigationPolicy} from './connection_navigation_policy.js'; -import {FieldNavigationPolicy} from './field_navigation_policy.js'; -import {FlyoutButtonNavigationPolicy} from './flyout_button_navigation_policy.js'; -import {FlyoutNavigationPolicy} from './flyout_navigation_policy.js'; -import {FlyoutSeparatorNavigationPolicy} from './flyout_separator_navigation_policy.js'; import {Marker} from './marker.js'; -import {WorkspaceNavigationPolicy} from './workspace_navigation_policy.js'; - -/** Options object for LineCursor instances. */ -export interface CursorOptions { - /** - * Can the cursor visit all stack connections (next/previous), or - * (if false) only unconnected next connections? - */ - stackConnections: boolean; -} - -/** Default options for LineCursor instances. */ -const defaultOptions: CursorOptions = { - stackConnections: true, -}; /** * Class for a line cursor. @@ -58,76 +27,14 @@ const defaultOptions: CursorOptions = { export class LineCursor extends Marker { override type = 'cursor'; - /** Options for this line cursor. */ - private readonly options: CursorOptions; - /** Locations to try moving the cursor to after a deletion. */ - private potentialNodes: INavigable[] | null = null; - - /** Whether the renderer is zelos-style. */ - private isZelos = false; + private potentialNodes: IFocusableNode[] | null = null; /** * @param workspace The workspace this cursor belongs to. - * @param options Cursor options. */ - constructor( - protected readonly workspace: WorkspaceSvg, - options?: Partial, - ) { + constructor(protected readonly workspace: WorkspaceSvg) { super(); - // Regularise options and apply defaults. - this.options = {...defaultOptions, ...options}; - - this.isZelos = workspace.getRenderer() instanceof Renderer; - - this.registerNavigationPolicies(); - } - - /** - * Registers default navigation policies for Blockly's built-in types with - * this cursor's workspace. - */ - protected registerNavigationPolicies() { - const navigator = this.workspace.getNavigator(); - - const blockPolicy = new BlockNavigationPolicy(); - if (this.workspace.isFlyout) { - const flyout = this.workspace.targetWorkspace?.getFlyout(); - if (flyout) { - navigator.set( - BlockSvg, - new FlyoutNavigationPolicy(blockPolicy, flyout), - ); - - const buttonPolicy = new FlyoutButtonNavigationPolicy(); - navigator.set( - FlyoutButton, - new FlyoutNavigationPolicy(buttonPolicy, flyout), - ); - - navigator.set( - FlyoutSeparator, - new FlyoutNavigationPolicy( - new FlyoutSeparatorNavigationPolicy(), - flyout, - ), - ); - } - } else { - navigator.set(BlockSvg, blockPolicy); - } - - navigator.set(RenderedConnection, new ConnectionNavigationPolicy()); - navigator.set(WorkspaceSvg, new WorkspaceNavigationPolicy()); - - const fieldPolicy = new FieldNavigationPolicy(); - navigator.set(FieldCheckbox, fieldPolicy); - navigator.set(FieldDropdown, fieldPolicy); - navigator.set(FieldImage, fieldPolicy); - navigator.set(FieldLabel, fieldPolicy); - navigator.set(FieldNumber, fieldPolicy); - navigator.set(FieldTextInput, fieldPolicy); } /** @@ -137,14 +44,14 @@ export class LineCursor extends Marker { * @returns The next node, or null if the current node is * not set or there is no next value. */ - next(): INavigable | null { + next(): IFocusableNode | null { const curNode = this.getCurNode(); if (!curNode) { return null; } const newNode = this.getNextNode( curNode, - (candidate: INavigable | null) => { + (candidate: IFocusableNode | null) => { return ( candidate instanceof BlockSvg && !candidate.outputConnection?.targetBlock() @@ -166,7 +73,7 @@ export class LineCursor extends Marker { * @returns The next node, or null if the current node is * not set or there is no next value. */ - in(): INavigable | null { + in(): IFocusableNode | null { const curNode = this.getCurNode(); if (!curNode) { return null; @@ -186,14 +93,14 @@ export class LineCursor extends Marker { * @returns The previous node, or null if the current node * is not set or there is no previous value. */ - prev(): INavigable | null { + prev(): IFocusableNode | null { const curNode = this.getCurNode(); if (!curNode) { return null; } const newNode = this.getPreviousNode( curNode, - (candidate: INavigable | null) => { + (candidate: IFocusableNode | null) => { return ( candidate instanceof BlockSvg && !candidate.outputConnection?.targetBlock() @@ -215,7 +122,7 @@ export class LineCursor extends Marker { * @returns The previous node, or null if the current node * is not set or there is no previous value. */ - out(): INavigable | null { + out(): IFocusableNode | null { const curNode = this.getCurNode(); if (!curNode) { return null; @@ -241,7 +148,7 @@ export class LineCursor extends Marker { const inNode = this.getNextNode(curNode, () => true, true); const nextNode = this.getNextNode( curNode, - (candidate: INavigable | null) => { + (candidate: IFocusableNode | null) => { return ( candidate instanceof BlockSvg && !candidate.outputConnection?.targetBlock() @@ -265,10 +172,10 @@ export class LineCursor extends Marker { * @returns The next node in the traversal. */ private getNextNodeImpl( - node: INavigable | null, - isValid: (p1: INavigable | null) => boolean, - visitedNodes: Set> = new Set>(), - ): INavigable | null { + node: IFocusableNode | null, + isValid: (p1: IFocusableNode | null) => boolean, + visitedNodes: Set = new Set(), + ): IFocusableNode | null { if (!node || visitedNodes.has(node)) return null; let newNode = this.workspace.getNavigator().getFirstChild(node) || @@ -301,10 +208,10 @@ export class LineCursor extends Marker { * @returns The next node in the traversal. */ getNextNode( - node: INavigable | null, - isValid: (p1: INavigable | null) => boolean, + node: IFocusableNode | null, + isValid: (p1: IFocusableNode | null) => boolean, loop: boolean, - ): INavigable | null { + ): IFocusableNode | null { if (!node || (!loop && this.getLastNode() === node)) return null; return this.getNextNodeImpl(node, isValid); @@ -323,10 +230,10 @@ export class LineCursor extends Marker { * exists. */ private getPreviousNodeImpl( - node: INavigable | null, - isValid: (p1: INavigable | null) => boolean, - visitedNodes: Set> = new Set>(), - ): INavigable | null { + node: IFocusableNode | null, + isValid: (p1: IFocusableNode | null) => boolean, + visitedNodes: Set = new Set(), + ): IFocusableNode | null { if (!node || visitedNodes.has(node)) return null; const newNode = @@ -355,10 +262,10 @@ export class LineCursor extends Marker { * exists. */ getPreviousNode( - node: INavigable | null, - isValid: (p1: INavigable | null) => boolean, + node: IFocusableNode | null, + isValid: (p1: IFocusableNode | null) => boolean, loop: boolean, - ): INavigable | null { + ): IFocusableNode | null { if (!node || (!loop && this.getFirstNode() === node)) return null; return this.getPreviousNodeImpl(node, isValid); @@ -371,15 +278,15 @@ export class LineCursor extends Marker { * @returns The right most child of the given node, or the node if no child * exists. */ - getRightMostChild( - node: INavigable | null, - stopIfFound: INavigable, - ): INavigable | null { + private getRightMostChild( + node: IFocusableNode | null, + stopIfFound: IFocusableNode, + ): IFocusableNode | null { if (!node) return node; let newNode = this.workspace.getNavigator().getFirstChild(node); if (!newNode || newNode === stopIfFound) return node; for ( - let nextNode: INavigable | null = newNode; + let nextNode: IFocusableNode | null = newNode; nextNode; nextNode = this.workspace.getNavigator().getNextSibling(newNode) ) { @@ -414,7 +321,7 @@ export class LineCursor extends Marker { preDelete(deletedBlock: BlockSvg) { const curNode = this.getCurNode(); - const nodes: INavigable[] = curNode ? [curNode] : []; + const nodes: IFocusableNode[] = curNode ? [curNode] : []; // The connection to which the deleted block is attached. const parentConnection = deletedBlock.previousConnection?.targetConnection ?? @@ -466,7 +373,7 @@ export class LineCursor extends Marker { * * @returns The current field, connection, or block the cursor is on. */ - override getCurNode(): INavigable | null { + override getCurNode(): IFocusableNode | null { this.updateCurNodeFromFocus(); return super.getCurNode(); } @@ -479,7 +386,7 @@ export class LineCursor extends Marker { * * @param newNode The new location of the cursor. */ - override setCurNode(newNode: INavigable | null) { + override setCurNode(newNode: IFocusableNode | null) { super.setCurNode(newNode); if (isFocusableNode(newNode)) { @@ -513,7 +420,7 @@ export class LineCursor extends Marker { * * @returns The first navigable node on the workspace, or null. */ - getFirstNode(): INavigable | null { + getFirstNode(): IFocusableNode | null { return this.workspace.getNavigator().getFirstChild(this.workspace); } @@ -522,7 +429,7 @@ export class LineCursor extends Marker { * * @returns The last navigable node on the workspace, or null. */ - getLastNode(): INavigable | null { + getLastNode(): IFocusableNode | null { const first = this.getFirstNode(); return this.getPreviousNode(first, () => true, true); } diff --git a/core/keyboard_nav/marker.ts b/core/keyboard_nav/marker.ts index 931f068b3da..5c2e7e9468b 100644 --- a/core/keyboard_nav/marker.ts +++ b/core/keyboard_nav/marker.ts @@ -14,7 +14,7 @@ import {BlockSvg} from '../block_svg.js'; import {Field} from '../field.js'; -import type {INavigable} from '../interfaces/i_navigable.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import {RenderedConnection} from '../rendered_connection.js'; /** @@ -26,7 +26,7 @@ export class Marker { colour: string | null = null; /** The current location of the marker. */ - protected curNode: INavigable | null = null; + protected curNode: IFocusableNode | null = null; /** The type of the marker. */ type = 'marker'; @@ -36,7 +36,7 @@ export class Marker { * * @returns The current field, connection, or block the marker is on. */ - getCurNode(): INavigable | null { + getCurNode(): IFocusableNode | null { return this.curNode; } @@ -45,7 +45,7 @@ export class Marker { * * @param newNode The new location of the marker, or null to remove it. */ - setCurNode(newNode: INavigable | null) { + setCurNode(newNode: IFocusableNode | null) { this.curNode = newNode; } @@ -59,7 +59,7 @@ export class Marker { * * @returns The parent block of the node if any, otherwise null. */ - getSourceBlockFromNode(node: INavigable | null): BlockSvg | null { + getSourceBlockFromNode(node: IFocusableNode | null): BlockSvg | null { if (node instanceof BlockSvg) { return node; } else if (node instanceof Field) { diff --git a/core/keyboard_nav/workspace_navigation_policy.ts b/core/keyboard_nav/workspace_navigation_policy.ts index ed7dc5d0236..12a7555b43f 100644 --- a/core/keyboard_nav/workspace_navigation_policy.ts +++ b/core/keyboard_nav/workspace_navigation_policy.ts @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {INavigable} from '../interfaces/i_navigable.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; -import type {WorkspaceSvg} from '../workspace_svg.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; /** * Set of rules controlling keyboard navigation from a workspace. @@ -20,7 +20,7 @@ export class WorkspaceNavigationPolicy * @param current The workspace to return the first child of. * @returns The top block of the first block stack, if any. */ - getFirstChild(current: WorkspaceSvg): INavigable | null { + getFirstChild(current: WorkspaceSvg): IFocusableNode | null { const blocks = current.getTopBlocks(true); return blocks.length ? blocks[0] : null; } @@ -31,7 +31,7 @@ export class WorkspaceNavigationPolicy * @param _current The workspace to return the parent of. * @returns Null. */ - getParent(_current: WorkspaceSvg): INavigable | null { + getParent(_current: WorkspaceSvg): IFocusableNode | null { return null; } @@ -41,7 +41,7 @@ export class WorkspaceNavigationPolicy * @param _current The workspace to return the next sibling of. * @returns Null. */ - getNextSibling(_current: WorkspaceSvg): INavigable | null { + getNextSibling(_current: WorkspaceSvg): IFocusableNode | null { return null; } @@ -51,7 +51,7 @@ export class WorkspaceNavigationPolicy * @param _current The workspace to return the previous sibling of. * @returns Null. */ - getPreviousSibling(_current: WorkspaceSvg): INavigable | null { + getPreviousSibling(_current: WorkspaceSvg): IFocusableNode | null { return null; } @@ -64,4 +64,14 @@ export class WorkspaceNavigationPolicy isNavigable(current: WorkspaceSvg): boolean { return current.canBeFocused(); } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a WorkspaceSvg. + */ + isApplicable(current: any): current is WorkspaceSvg { + return current instanceof WorkspaceSvg; + } } diff --git a/core/navigator.ts b/core/navigator.ts index ffff2507c3a..7a1c2d4ea10 100644 --- a/core/navigator.ts +++ b/core/navigator.ts @@ -4,10 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {INavigable} from './interfaces/i_navigable.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {INavigationPolicy} from './interfaces/i_navigation_policy.js'; +import {BlockNavigationPolicy} from './keyboard_nav/block_navigation_policy.js'; +import {ConnectionNavigationPolicy} from './keyboard_nav/connection_navigation_policy.js'; +import {FieldNavigationPolicy} from './keyboard_nav/field_navigation_policy.js'; +import {WorkspaceNavigationPolicy} from './keyboard_nav/workspace_navigation_policy.js'; -type RuleMap = Map T, INavigationPolicy>; +type RuleList = INavigationPolicy[]; /** * Class responsible for determining where focus should move in response to @@ -18,35 +22,35 @@ export class Navigator { * Map from classes to a corresponding ruleset to handle navigation from * instances of that class. */ - private rules: RuleMap = new Map(); + protected rules: RuleList = [ + new BlockNavigationPolicy(), + new FieldNavigationPolicy(), + new ConnectionNavigationPolicy(), + new WorkspaceNavigationPolicy(), + ]; /** - * Associates a navigation ruleset with its corresponding class. + * Adds a navigation ruleset to this Navigator. * - * @param key The class whose object instances should have their navigation - * controlled by the associated policy. * @param policy A ruleset that determines where focus should move starting - * from an instance of the given class. + * from an instance of its managed class. */ - set>( - key: new (...args: any) => T, - policy: INavigationPolicy, - ) { - this.rules.set(key, policy); + addNavigationPolicy(policy: INavigationPolicy) { + this.rules.push(policy); } /** * Returns the navigation ruleset associated with the given object instance's * class. * - * @param key An object to retrieve a navigation ruleset for. + * @param current An object to retrieve a navigation ruleset for. * @returns The navigation ruleset of objects of the given object's class, or * undefined if no ruleset has been registered for the object's class. */ - private get>( - key: T, - ): INavigationPolicy | undefined { - return this.rules.get(key.getClass()); + private get( + current: IFocusableNode, + ): INavigationPolicy | undefined { + return this.rules.find((rule) => rule.isApplicable(current)); } /** @@ -55,7 +59,7 @@ export class Navigator { * @param current The object to retrieve the first child of. * @returns The first child node of the given object, if any. */ - getFirstChild>(current: T): INavigable | null { + getFirstChild(current: IFocusableNode): IFocusableNode | null { const result = this.get(current)?.getFirstChild(current); if (!result) return null; // If the child isn't navigable, don't traverse into it; check its peers. @@ -71,7 +75,7 @@ export class Navigator { * @param current The object to retrieve the parent of. * @returns The parent node of the given object, if any. */ - getParent>(current: T): INavigable | null { + getParent(current: IFocusableNode): IFocusableNode | null { const result = this.get(current)?.getParent(current); if (!result) return null; if (!this.get(result)?.isNavigable(result)) return this.getParent(result); @@ -84,7 +88,7 @@ export class Navigator { * @param current The object to retrieve the next sibling node of. * @returns The next sibling node of the given object, if any. */ - getNextSibling>(current: T): INavigable | null { + getNextSibling(current: IFocusableNode): IFocusableNode | null { const result = this.get(current)?.getNextSibling(current); if (!result) return null; if (!this.get(result)?.isNavigable(result)) { @@ -99,9 +103,7 @@ export class Navigator { * @param current The object to retrieve the previous sibling node of. * @returns The previous sibling node of the given object, if any. */ - getPreviousSibling>( - current: T, - ): INavigable | null { + getPreviousSibling(current: IFocusableNode): IFocusableNode | null { const result = this.get(current)?.getPreviousSibling(current); if (!result) return null; if (!this.get(result)?.isNavigable(result)) { diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index 4298afa3f23..84905eeccc2 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -24,7 +24,6 @@ import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {hasBubble} from './interfaces/i_has_bubble.js'; -import type {INavigable} from './interfaces/i_navigable.js'; import * as internalConstants from './internal_constants.js'; import {Coordinate} from './utils/coordinate.js'; import * as svgMath from './utils/svg_math.js'; @@ -38,7 +37,7 @@ const BUMP_RANDOMNESS = 10; */ export class RenderedConnection extends Connection - implements IContextMenu, IFocusableNode, INavigable + implements IContextMenu, IFocusableNode { // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. sourceBlock_!: BlockSvg; @@ -664,15 +663,6 @@ export class RenderedConnection | unknown | null as SVGElement | null; } - - /** - * Returns this connection's class for keyboard navigation. - * - * @returns RenderedConnection. - */ - getClass() { - return RenderedConnection; - } } export namespace RenderedConnection { diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 3095f092474..5caa56818a2 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -53,7 +53,6 @@ import { import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {hasBubble} from './interfaces/i_has_bubble.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; -import type {INavigable} from './interfaces/i_navigable.js'; import type {IToolbox} from './interfaces/i_toolbox.js'; import type {LineCursor} from './keyboard_nav/line_cursor.js'; import type {Marker} from './keyboard_nav/marker.js'; @@ -100,11 +99,7 @@ const ZOOM_TO_FIT_MARGIN = 20; */ export class WorkspaceSvg extends Workspace - implements - IContextMenu, - IFocusableNode, - IFocusableTree, - INavigable + implements IContextMenu, IFocusableNode, IFocusableTree { /** * A wrapper function called when a resize event occurs. @@ -2823,15 +2818,6 @@ export class WorkspaceSvg } } - /** - * Returns the class of this workspace. - * - * @returns WorkspaceSvg. - */ - getClass() { - return WorkspaceSvg; - } - /** * Returns an object responsible for coordinating movement of focus between * items on this workspace in response to keyboard navigation commands. @@ -2841,6 +2827,16 @@ export class WorkspaceSvg getNavigator(): Navigator { return this.navigator; } + + /** + * Sets the Navigator instance used by this workspace. + * + * @param newNavigator A Navigator object to coordinate movement between + * elements on the workspace. + */ + setNavigator(newNavigator: Navigator) { + this.navigator = newNavigator; + } } /** diff --git a/tests/typescript/src/field/different_user_input.ts b/tests/typescript/src/field/different_user_input.ts index 3083fe5545e..f91f25bbc86 100644 --- a/tests/typescript/src/field/different_user_input.ts +++ b/tests/typescript/src/field/different_user_input.ts @@ -61,10 +61,6 @@ class FieldMitosis extends Field { }); this.value_ = {cells}; } - - getClass() { - return FieldMitosis; - } } fieldRegistry.register('field_mitosis', FieldMitosis); From 2b9d06ac9905633920f40852967b0ad39ca1e13b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 14 May 2025 10:46:22 -0700 Subject: [PATCH 209/222] fix: Use a unique focus ID for BlockSvg. (#9045) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #9043 Fixes https://github.com/google/blockly-samples/issues/2512 ### Proposed Changes This replaces using BlockSvg's own ID for focus management since that's not guaranteed to be unique across all workspaces on the page. ### Reason for Changes Both https://github.com/google/blockly-samples/issues/2512 covers the user-facing issue in more detail, but from a technical perspective it's possible for blocks to share IDs across workspaces. One easy demonstration of this is the flyout: the first block created from the flyout to the main workspace will share an ID. The workspace minimap plugin just makes the underlying problem more obvious. The reason this introduces a breakage is due to the inherent ordering that `FocusManager` uses when trying to find a matching tree for a given DOM element that has received focus. These trees are iterated in the order of their registration, so it's quite possible for some cases (like main workspace vs. flyout) to resolve such that the behavior looks correct to users, vs. others (such as the workspace minimap) not behaving as expected. Guaranteeing ID uniqueness across all workspaces fixes the problem entirely. ### Test Coverage This has been manually tested in core Blockly's simple test playground and in Blockly samples' workspace minimap plugin test environment (linked against this change). See the new behavior for the minimap plugin: [Screen recording 2025-05-13 4.31.31 PM.webm](https://github.com/user-attachments/assets/d2ec3621-6e86-4932-ae85-333b0e7015e1) Note that this is a regression to v11 behavior in that the blocks in the minimap now show as selected. This has been verified as working with the latest version of the keyboard navigation plugin (tip-of-tree). Keyboard-based block operations and movement seem to work as expected. For automated testing this is expected to largely be covered by future tests added as part of resolving #8915. ### Documentation No public documentation changes should be needed, though `IFocusableNode`'s documentation has been refined to be clearer on the uniqueness property for focusable element IDs. ### Additional Information There's a separate open design question here about whether `BlockSvg`'s descendants should use the new focus ID vs. the block ID. Here is what I consider to be the trade-off analysis in this decision: | | Pros | Cons | |------------------------|-------------------------------------------------|------------------------------------------------------------------------------| | Use `BlockSvg.id` | Can use fast `WorkspaceSvg.getBlockById`. | `WorkspaceSvg.lookUpFocusableNode` now uses 2 different IDs. | | Use `BlockSvg.focusId` | Consistency in IDs use for block-related focus. | Requires more expensive block look-up in `WorkspaceSvg.lookUpFocusableNode`. | --- core/block_svg.ts | 5 ++++- core/interfaces/i_focusable_node.ts | 12 +++++++----- core/workspace_svg.ts | 4 +++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index ca25560fd01..ea5dd7da7ed 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -55,6 +55,7 @@ import type {IPathObject} from './renderers/common/i_path_object.js'; import * as blocks from './serialization/blocks.js'; import type {BlockStyle} from './theme.js'; import * as Tooltip from './tooltip.js'; +import {idGenerator} from './utils.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import {Rect} from './utils/rect.js'; @@ -210,7 +211,9 @@ export class BlockSvg // Expose this block's ID on its top-level SVG group. this.svgGroup.setAttribute('data-id', this.id); - svgPath.id = this.id; + + // The page-wide unique ID of this Block used for focusing. + svgPath.id = idGenerator.getNextUniqueId(); this.doInit_(); } diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts index 6844e080941..b21d7741a5c 100644 --- a/core/interfaces/i_focusable_node.ts +++ b/core/interfaces/i_focusable_node.ts @@ -19,11 +19,13 @@ export interface IFocusableNode { * - blocklyActiveFocus * - blocklyPassiveFocus * - * The returned element must also have a valid ID specified, and unique to the - * element relative to its nearest IFocusableTree parent. It must also have a - * negative tabindex (since the focus manager itself will manage its tab index - * and a tab index must be present in order for the element to be focusable in - * the DOM). + * The returned element must also have a valid ID specified, and unique across + * the entire page. Failing to have a properly unique ID could result in + * trying to focus one node (such as via a mouse click) leading to another + * node with the same ID actually becoming focused by FocusManager. The + * returned element must also have a negative tabindex (since the focus + * manager itself will manage its tab index and a tab index must be present in + * order for the element to be focusable in the DOM). * * The returned element must be visible if the node is ever focused via * FocusManager.focusNode() or FocusManager.focusTree(). It's allowed for an diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 5caa56818a2..3e8731afd4b 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2759,7 +2759,9 @@ export class WorkspaceSvg } // Search for a specific block. - const block = this.getBlockById(id); + const block = this.getAllBlocks(false).find( + (block) => block.getFocusableElement().id === id, + ); if (block) return block; // Search for a workspace comment (semi-expensive). From e1179808fd3bf0908adfefc36ab367cfb33feee0 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 14 May 2025 10:50:00 -0700 Subject: [PATCH 210/222] fix: Ensure cursor syncs with more than just focused blocks (#9032) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes https://github.com/google/blockly-keyboard-experimentation/issues/499 ### Proposed Changes This ensures that non-blocks which hold active focus correctly update `LineCursor`'s internal state. ### Reason for Changes This is outright a correction in how `LineCursor` has worked up until now, and is now possible after several recent changes (most notably #9004). #9004 updated selection to be more explicitly generic (and based on `IFocusableNode`) which means `LineCursor` should also properly support more than just blocks when synchronizing with focus (in place of selection), particularly since lots of non-block things can be focusable. What's interesting is that this change isn't strictly necessary, even if it is a reasonable correction and improvement in the robustness of `LineCursor`. Essentially everywhere navigation is handled results in a call to `setCurNode` which correctly sets the cursor's internal state (with no specific correction from focus since only blocks were checked and we already ensure that selecting a block correctly focuses it). ### Test Coverage It would be nice to add test coverage specifically for the cursor cases, but it seems largely unnecessary since: 1. The main failure cases are test-specific (as mentioned above). 2. These flows are better left tested as part of broader accessibility testing (per #8915). This has been tested with a cursory playthrough of some basic scenarios (block movement, insertion, deletion, copy & paste, context menus, and interacting with fields). ### Documentation No new documentation should be needed here. ### Additional Information This is expected to only affect keyboard navigation plugin behaviors, particularly plugin tests. It may be worth updating `LineCursor` to completely reflect current focus state rather than holding an internal variable. This, in turn, may end up simplifying solving issues like #8793 (but not necessarily). --- core/keyboard_nav/line_cursor.ts | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 71c61b4f3e4..9d83f6554d3 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -374,7 +374,15 @@ export class LineCursor extends Marker { * @returns The current field, connection, or block the cursor is on. */ override getCurNode(): IFocusableNode | null { - this.updateCurNodeFromFocus(); + // Ensure the current node matches what's currently focused. + const focused = getFocusManager().getFocusedNode(); + const block = this.getSourceBlockFromNode(focused); + if (!block || block.workspace === this.workspace) { + // If the current focused node corresponds to a block then ensure that it + // belongs to the correct workspace for this cursor. + this.setCurNode(focused); + } + return super.getCurNode(); } @@ -401,20 +409,6 @@ export class LineCursor extends Marker { } } - /** - * Updates the current node to match what's currently focused. - */ - private updateCurNodeFromFocus() { - const focused = getFocusManager().getFocusedNode(); - - if (focused instanceof BlockSvg) { - const block: BlockSvg | null = focused; - if (block && block.workspace === this.workspace) { - this.setCurNode(block); - } - } - } - /** * Get the first navigable node on the workspace, or null if none exist. * From 7a7fad45c1c4c43d3329c5eeaaf0d08ee08c6f1f Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 14 May 2025 11:23:12 -0700 Subject: [PATCH 211/222] fix: Reenable support for tabbing between fields. (#9049) * fix: Reenable support for tabbing between fields. * refactor: Reduce code duplication. --- core/field_input.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/core/field_input.ts b/core/field_input.ts index f329e0991dc..6d9c3b99339 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -27,6 +27,7 @@ import { FieldValidator, UnattachedFieldError, } from './field.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import {Msg} from './msg.js'; import * as renderManagement from './render_management.js'; import * as aria from './utils/aria.js'; @@ -582,6 +583,28 @@ export abstract class FieldInput extends Field< ); WidgetDiv.hideIfOwner(this); dropDownDiv.hideWithoutAnimation(); + } else if (e.key === 'Tab') { + e.preventDefault(); + const cursor = this.workspace_?.getCursor(); + + const isValidDestination = (node: IFocusableNode | null) => + (node instanceof FieldInput || + (node instanceof BlockSvg && node.isSimpleReporter())) && + node !== this.getSourceBlock(); + + let target = e.shiftKey + ? cursor?.getPreviousNode(this, isValidDestination, false) + : cursor?.getNextNode(this, isValidDestination, false); + target = + target instanceof BlockSvg && target.isSimpleReporter() + ? target.getFields().next().value + : target; + + if (target instanceof FieldInput) { + WidgetDiv.hideIfOwner(this); + dropDownDiv.hideWithoutAnimation(); + target.showEditor(); + } } } From 523dca92bd4492115e58686e487b6aba3cfd5dcf Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Wed, 14 May 2025 12:22:09 -0700 Subject: [PATCH 212/222] fix: fieldDropdown.getText works in node (#9048) * fix: dropdown getText works in node * chore: comment --- core/field_dropdown.ts | 23 ++++++++++++++++++++--- tests/node/run_node_test.mjs | 7 +++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 0b787bd7701..b576685168a 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -633,7 +633,13 @@ export class FieldDropdown extends Field { /** * Use the `getText_` developer hook to override the field's text * representation. Get the selected option text. If the selected option is - * an image we return the image alt text. + * an image we return the image alt text. If the selected option is + * an HTMLElement, return the title, ariaLabel, or innerText of the + * element. + * + * If you use HTMLElement options in Node.js and call this function, + * ensure that you are supplying an implementation of HTMLElement, + * such as through jsdom-global. * * @returns Selected option text. */ @@ -644,10 +650,21 @@ export class FieldDropdown extends Field { const option = this.selectedOption[0]; if (isImageProperties(option)) { return option.alt; - } else if (option instanceof HTMLElement) { + } else if ( + typeof HTMLElement !== 'undefined' && + option instanceof HTMLElement + ) { return option.title ?? option.ariaLabel ?? option.innerText; + } else if (typeof option === 'string') { + return option; } - return option; + + console.warn( + "Can't get text for existing dropdown option. If " + + "you're using HTMLElement dropdown options in node, ensure you're " + + 'using jsdom-global or similar.', + ); + return null; } /** diff --git a/tests/node/run_node_test.mjs b/tests/node/run_node_test.mjs index f32286bbafb..c6d21506987 100644 --- a/tests/node/run_node_test.mjs +++ b/tests/node/run_node_test.mjs @@ -181,4 +181,11 @@ suite('Test Node.js', function () { assert.deepEqual(jsonAfter, json); }); + test('Dropdown getText works with no HTMLElement defined', function () { + const field = new Blockly.FieldDropdown([ + ['firstOption', '1'], + ['secondOption', '2'], + ]); + assert.equal(field.getText(), 'firstOption'); + }); }); From 205ef6c7d78bdcf111369dcb8d7fd9ed1a327c4f Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Wed, 14 May 2025 12:23:12 -0700 Subject: [PATCH 213/222] fix!: deepMerge for arrays, shortcut keycodes returned as array (#9047) --- core/utils/object.ts | 7 ++++++- tests/mocha/shortcut_registry_test.js | 6 +++++- tests/mocha/utils_test.js | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/core/utils/object.ts b/core/utils/object.ts index 6aaabcc90f1..5cf0b49f2b9 100644 --- a/core/utils/object.ts +++ b/core/utils/object.ts @@ -9,6 +9,9 @@ /** * Complete a deep merge of all members of a source object with a target object. * + * N.B. This is not a very sophisticated merge algorithm and does not + * handle complex cases. Use with caution. + * * @param target Target. * @param source Source. * @returns The resulting object. @@ -18,7 +21,9 @@ export function deepMerge( source: AnyDuringMigration, ): AnyDuringMigration { for (const x in source) { - if (source[x] !== null && typeof source[x] === 'object') { + if (source[x] !== null && Array.isArray(source[x])) { + target[x] = deepMerge(target[x] || [], source[x]); + } else if (source[x] !== null && typeof source[x] === 'object') { target[x] = deepMerge(target[x] || Object.create(null), source[x]); } else { target[x] = source[x]; diff --git a/tests/mocha/shortcut_registry_test.js b/tests/mocha/shortcut_registry_test.js index 9f412be3317..5641e17c7d1 100644 --- a/tests/mocha/shortcut_registry_test.js +++ b/tests/mocha/shortcut_registry_test.js @@ -260,7 +260,7 @@ suite('Keyboard Shortcut Registry Test', function () { assert.equal(this.registry.getKeyMap()['keyCode'][0], 'a'); }); test('Gets a copy of the registry', function () { - const shortcut = {'name': 'shortcutName'}; + const shortcut = {'name': 'shortcutName', 'keyCodes': ['2', '4']}; this.registry.register(shortcut); const registrycopy = this.registry.getRegistry(); registrycopy['shortcutName']['name'] = 'shortcutName1'; @@ -268,6 +268,10 @@ suite('Keyboard Shortcut Registry Test', function () { this.registry.getRegistry()['shortcutName']['name'], 'shortcutName', ); + assert.deepEqual( + this.registry.getRegistry()['shortcutName']['keyCodes'], + shortcut['keyCodes'], + ); }); test('Gets keyboard shortcuts from a key code', function () { this.registry.setKeyMap({'keyCode': ['shortcutName']}); diff --git a/tests/mocha/utils_test.js b/tests/mocha/utils_test.js index 97984fdbae4..b6228448db7 100644 --- a/tests/mocha/utils_test.js +++ b/tests/mocha/utils_test.js @@ -533,4 +533,25 @@ suite('Utils', function () { assert.equal(Blockly.utils.math.toDegrees(5 * quarter), 360 + 90, '450'); }); }); + + suite('deepMerge', function () { + test('Merges two objects', function () { + const target = {a: 1, b: '2', shared: 'this should be overwritten'}; + const source = {c: {deeplyNested: true}, shared: 'I overwrote it'}; + + const expected = {...target, ...source}; + const actual = Blockly.utils.object.deepMerge(target, source); + + assert.deepEqual(expected, actual); + }); + test('Merges objects with arrays', function () { + const target = {a: 1}; + const source = {b: ['orange', 'lime']}; + + const expected = {...target, ...source}; + const actual = Blockly.utils.object.deepMerge(target, source); + + assert.deepEqual(expected, actual); + }); + }); }); From cfa625af3256dd3e06139b9b434eee141135ef58 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 14 May 2025 12:31:03 -0700 Subject: [PATCH 214/222] release: Update version number to 12.0.0-beta.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53de669f5fb..32428f49d74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockly", - "version": "12.0.0-beta.4", + "version": "12.0.0-beta.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "12.0.0-beta.4", + "version": "12.0.0-beta.6", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 02de252439d..151f43bb62b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "12.0.0-beta.5", + "version": "12.0.0-beta.6", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly" From 4a2b743f10f1b1f628bfba12d5865a1201166e92 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 14 May 2025 14:50:23 -0700 Subject: [PATCH 215/222] fix: Fix bug when referencing HTMLElement in non-browser environments. (#9050) --- core/field_dropdown.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index b576685168a..9c0d7f29269 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -800,7 +800,9 @@ export class FieldDropdown extends Field { option[0] && typeof option[0] !== 'string' && !isImageProperties(option[0]) && - !(option[0] instanceof HTMLElement) + !( + typeof HTMLElement !== 'undefined' && option[0] instanceof HTMLElement + ) ) { foundError = true; console.error( From 083329aaa5557c2dd265fd6e9fef0614e4c2c50f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 14 May 2025 15:35:07 -0700 Subject: [PATCH 216/222] feat: Add support for conditional ephemeral focus. (#9051) ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Needed for fixing https://github.com/google/blockly-samples/issues/2514 and https://github.com/google/blockly-samples/issues/2515. ### Proposed Changes Update `FieldInput` along with drop-down and widget divs to support disabling the automatic ephemeral focus functionality. ### Reason for Changes As mentioned in https://github.com/google/blockly-samples/issues/2514#issuecomment-2881539117 both https://github.com/google/blockly-samples/issues/2514 and https://github.com/google/blockly-samples/issues/2515 were caused by the custom fields leading to both drop-down and widget divs simultaneously taking ephemeral focus (and that's not currently allowed by `FocusManager`). This change updates both widget and drop-down divs with _optional_ parameters to conditionally disable automatic ephemeral focus so that `FieldInput` can, in turn, be customized with disabling automatic ephemeral focus for its inline editor. Being able to disable ephemeral focus for `FieldInput` allows the custom fields' own drop-down divs to take and manage ephemeral focus, instead, avoiding the duplicate scenario that led to the runtime failure. Note that the drop-down div change in this PR is completely optional, but it's added for consistency and to avoid future scenarios of breakage when trying to use both divs together (as a fix is required in Core without monkeypatching). It's worth noting that there could be a possibility for a more 'proper' fix in `FocusManager` by allowing multiple calls to `takeEphemeralFocus`, but it's unclear exactly how to solve this consistently (which is why it results in a hard failure today). The main issue is that the current focused node can change from underneath the manager (due to DOM focus changes), and the current focused element may also change. It's not clear if the first, last, or some other call to `takeEphemeralFocus` should take precedent or which node to return focus once ephemeral focus ends (in cases with multiple subsequent calls). ### Test Coverage No new tests were added, though common field cases were tested manually in core's simple playground and in the plugin-specific playgrounds (per the original regressions). The keyboard navigation plugin test environment was also verified to ensure that this didn't alter any existing behavior (it should be a no-op except for the two custom field plugins). Automated tests would be nice to add for all three classes, perhaps as part of #8915. ### Documentation The code documentation changes here should be sufficient. ### Additional Information These changes are being done directly to ease solving the above samples issues. See https://github.com/google/blockly-samples/pull/2521 for the follow-up fixes to samples. --- core/dropdowndiv.ts | 14 +++++++++++++- core/field_input.ts | 17 ++++++++++++++--- core/widgetdiv.ts | 10 +++++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/core/dropdowndiv.ts b/core/dropdowndiv.ts index e326ac94cb4..894724d4448 100644 --- a/core/dropdowndiv.ts +++ b/core/dropdowndiv.ts @@ -268,6 +268,11 @@ function getScaledBboxOfField(field: Field): Rect { * @param field The field to position the dropdown against. * @param opt_onHide Optional callback for when the drop-down is hidden. * @param opt_secondaryYOffset Optional Y offset for above-block positioning. + * @param manageEphemeralFocus Whether ephemeral focus should be managed + * according to the drop-down div's lifetime. Note that if a false value is + * passed in here then callers should manage ephemeral focus directly + * otherwise focus may not properly restore when the widget closes. Defaults + * to true. * @returns True if the menu rendered below block; false if above. */ function showPositionedByRect( @@ -275,6 +280,7 @@ function showPositionedByRect( field: Field, opt_onHide?: () => void, opt_secondaryYOffset?: number, + manageEphemeralFocus: boolean = true, ): boolean { // If we can fit it, render below the block. const primaryX = bBox.left + (bBox.right - bBox.left) / 2; @@ -299,6 +305,7 @@ function showPositionedByRect( primaryY, secondaryX, secondaryY, + manageEphemeralFocus, opt_onHide, ); } @@ -319,6 +326,8 @@ function showPositionedByRect( * @param secondaryX Secondary/alternative origin point x, in absolute px. * @param secondaryY Secondary/alternative origin point y, in absolute px. * @param opt_onHide Optional callback for when the drop-down is hidden. + * @param manageEphemeralFocus Whether ephemeral focus should be managed + * according to the widget div's lifetime. * @returns True if the menu rendered at the primary origin point. * @internal */ @@ -329,6 +338,7 @@ export function show( primaryY: number, secondaryX: number, secondaryY: number, + manageEphemeralFocus: boolean, opt_onHide?: () => void, ): boolean { owner = newOwner as Field; @@ -342,7 +352,9 @@ export function show( dom.addClass(div, renderedClassName); dom.addClass(div, themeClassName); - returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div); + if (manageEphemeralFocus) { + returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div); + } // When we change `translate` multiple times in close succession, // Chrome may choose to wait and apply them all at once. diff --git a/core/field_input.ts b/core/field_input.ts index 6d9c3b99339..c7921d6f015 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -352,8 +352,16 @@ export abstract class FieldInput extends Field< * undefined if triggered programmatically. * @param quietInput True if editor should be created without focus. * Defaults to false. + * @param manageEphemeralFocus Whether ephemeral focus should be managed as + * part of the editor's inline editor (when the inline editor is shown). + * Callers must manage ephemeral focus themselves if this is false. + * Defaults to true. */ - protected override showEditor_(_e?: Event, quietInput = false) { + protected override showEditor_( + _e?: Event, + quietInput = false, + manageEphemeralFocus: boolean = true, + ) { this.workspace_ = (this.sourceBlock_ as BlockSvg).workspace; if ( !quietInput && @@ -362,7 +370,7 @@ export abstract class FieldInput extends Field< ) { this.showPromptEditor(); } else { - this.showInlineEditor(quietInput); + this.showInlineEditor(quietInput, manageEphemeralFocus); } } @@ -389,8 +397,10 @@ export abstract class FieldInput extends Field< * Create and show a text input editor that sits directly over the text input. * * @param quietInput True if editor should be created without focus. + * @param manageEphemeralFocus Whether ephemeral focus should be managed as + * part of the field's inline editor (widget div). */ - private showInlineEditor(quietInput: boolean) { + private showInlineEditor(quietInput: boolean, manageEphemeralFocus: boolean) { const block = this.getSourceBlock(); if (!block) { throw new UnattachedFieldError(); @@ -400,6 +410,7 @@ export abstract class FieldInput extends Field< block.RTL, this.widgetDispose_.bind(this), this.workspace_, + manageEphemeralFocus, ); this.htmlInput_ = this.widgetCreate_() as HTMLInputElement; this.isBeingEdited_ = true; diff --git a/core/widgetdiv.ts b/core/widgetdiv.ts index 608927b6fb0..936983e8f10 100644 --- a/core/widgetdiv.ts +++ b/core/widgetdiv.ts @@ -84,12 +84,18 @@ export function createDom() { * @param newDispose Optional cleanup function to be run when the widget is * closed. * @param workspace The workspace associated with the widget owner. + * @param manageEphemeralFocus Whether ephemeral focus should be managed + * according to the widget div's lifetime. Note that if a false value is + * passed in here then callers should manage ephemeral focus directly + * otherwise focus may not properly restore when the widget closes. Defaults + * to true. */ export function show( newOwner: unknown, rtl: boolean, newDispose: () => void, workspace?: WorkspaceSvg | null, + manageEphemeralFocus: boolean = true, ) { hide(); owner = newOwner; @@ -114,7 +120,9 @@ export function show( if (themeClassName) { dom.addClass(div, themeClassName); } - returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div); + if (manageEphemeralFocus) { + returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div); + } } /** From 215fad8676f01895b9e5bf08d6c87d80a59c3e81 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 14 May 2025 15:45:02 -0700 Subject: [PATCH 217/222] fix: Remove un-typesafe cast. (#9052) --- core/utils/dom.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/utils/dom.ts b/core/utils/dom.ts index 87019dbb2de..4087984151c 100644 --- a/core/utils/dom.ts +++ b/core/utils/dom.ts @@ -289,13 +289,13 @@ export function getFastTextWidthWithSizeString( // Initialize the HTML canvas context and set the font. // The context font must match blocklyText's fontsize and font-family // set in CSS. - canvasContext = computeCanvas.getContext('2d') as CanvasRenderingContext2D; + canvasContext = computeCanvas.getContext('2d'); } - // Set the desired font size and family. - canvasContext.font = fontWeight + ' ' + fontSize + ' ' + fontFamily; // Measure the text width using the helper canvas context. - if (text) { + if (text && canvasContext) { + // Set the desired font size and family. + canvasContext.font = fontWeight + ' ' + fontSize + ' ' + fontFamily; width = canvasContext.measureText(text).width; } else { width = 0; From 8c0ee9fa247bd41b2575b0242822592ab6a34e52 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Wed, 14 May 2025 17:15:27 -0700 Subject: [PATCH 218/222] release: update version number to 12.0.0-beta.7 (#9053) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 32428f49d74..9b90c92c41e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockly", - "version": "12.0.0-beta.6", + "version": "12.0.0-beta.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "12.0.0-beta.6", + "version": "12.0.0-beta.7", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 151f43bb62b..1de4129b99a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "12.0.0-beta.6", + "version": "12.0.0-beta.7", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly" From ad0563daf78c4502fb4e2bd98d597a4cc82392a3 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 15 May 2025 09:15:07 -0700 Subject: [PATCH 219/222] fix: Make clickable but non-editable fields navigable. (#9054) --- core/keyboard_nav/field_navigation_policy.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/keyboard_nav/field_navigation_policy.ts b/core/keyboard_nav/field_navigation_policy.ts index 9139b64aeed..3b88dc9248b 100644 --- a/core/keyboard_nav/field_navigation_policy.ts +++ b/core/keyboard_nav/field_navigation_policy.ts @@ -97,8 +97,7 @@ export class FieldNavigationPolicy implements INavigationPolicy> { isNavigable(current: Field): boolean { return ( current.canBeFocused() && - current.isClickable() && - current.isCurrentlyEditable() && + (current.isClickable() || current.isCurrentlyEditable()) && !( current.getSourceBlock()?.isSimpleReporter() && current.isFullBlockField() From c8ad30b9bf7c42dd4c913c32f2ea152fcffab5ea Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 15 May 2025 12:48:14 -0700 Subject: [PATCH 220/222] chore: Rollup of 2025 Q2 updates from TranslateWiki (#9056) * chore: Rollup of 2025 Q2 updates from TranslateWiki * fix: Readd inadvertently deleted message. * Fix: Readd another deleted message. * chore: Reformat files. * Fix: Revert some changes to en.json. --- msg/json/ar.json | 6 +++--- msg/json/az.json | 10 +++++---- msg/json/be-tarask.json | 11 +++++----- msg/json/be.json | 2 ++ msg/json/bg.json | 6 ++++-- msg/json/ce.json | 44 +++++++++++++++++++------------------ msg/json/constants.json | 15 ++++++------- msg/json/cs.json | 3 ++- msg/json/de.json | 4 ++-- msg/json/diq.json | 10 +++++++-- msg/json/es.json | 3 +++ msg/json/et.json | 9 ++++++++ msg/json/fa.json | 1 + msg/json/fr.json | 3 ++- msg/json/he.json | 3 ++- msg/json/hr.json | 10 +++++++++ msg/json/ia.json | 4 ++-- msg/json/id.json | 7 ++++-- msg/json/ja.json | 1 + msg/json/kn.json | 1 + msg/json/ko.json | 27 +++++++++++++---------- msg/json/lb.json | 2 +- msg/json/nb.json | 1 + msg/json/nl.json | 3 ++- msg/json/pa.json | 16 +++++++++----- msg/json/pl.json | 1 + msg/json/pms.json | 1 + msg/json/pt-br.json | 4 +++- msg/json/pt.json | 2 ++ msg/json/qqq.json | 5 +++-- msg/json/sd.json | 2 +- msg/json/sk.json | 7 ++++++ msg/json/sr.json | 1 + msg/json/sv.json | 5 ++++- msg/json/synonyms.json | 4 ++-- msg/json/tcy.json | 4 ++-- msg/json/tdd.json | 1 + msg/json/tl.json | 46 ++++++++++++++++++++++++++++----------- msg/json/tr.json | 1 + msg/json/ug-arab.json | 48 +++++++++++++++++++++++++++++++++++++++++ msg/json/uk.json | 1 + msg/json/ur.json | 1 + msg/json/xmf.json | 3 +++ msg/json/zgh.json | 6 +++--- msg/json/zh-hans.json | 6 ++++-- 45 files changed, 252 insertions(+), 99 deletions(-) diff --git a/msg/json/ar.json b/msg/json/ar.json index 2d382ddcf99..bad3d37ca13 100644 --- a/msg/json/ar.json +++ b/msg/json/ar.json @@ -1,7 +1,6 @@ { "@metadata": { "authors": [ - "Amire80", "Diyariq", "DonAdnan", "Dr-Taher", @@ -91,7 +90,7 @@ "CONTROLS_IF_TOOLTIP_4": "إذا كانت القيمة الأولى تساوي \"صحيح\", إذن قم بتنفيذ القطعة الأولى من الأوامر. والا , إذا كانت القيمة الثانية تساوي \"صحيح\", قم بتنفيذ القطعة الثانية من الأوامر. إذا لم تكن هناك أي قيمة تساوي صحيح, قم بتنفيذ آخر قطعة من الأوامر.", "CONTROLS_IF_MSG_IF": "إذا", "CONTROLS_IF_MSG_ELSEIF": "وإﻻ إذا", - "CONTROLS_IF_MSG_ELSE": "والا", + "CONTROLS_IF_MSG_ELSE": "و إلا", "CONTROLS_IF_IF_TOOLTIP": "أضف, إزل, أو أعد ترتيب المقاطع لإعادة تكوين القطعة الشرطية \"إذا\".", "CONTROLS_IF_ELSEIF_TOOLTIP": "إضف شرطا إلى القطعة الشرطية \"إذا\".", "CONTROLS_IF_ELSE_TOOLTIP": "أضف شرط \"نهاية، إجمع\" إلى القطعة الشرطية \"إذا\".", @@ -152,7 +151,7 @@ "MATH_CONSTANT_HELPURL": "https://ar.wikipedia.org/wiki/ثابت رياضي", "MATH_CONSTANT_TOOLTIP": "ير جع واحد من الثوابت الشائعة : π (3.141…), e (2.718…), φ (1.618…), sqrt(2) (1.414…), sqrt(½) (0.707…), or ∞ (infinity).", "MATH_IS_EVEN": "هو زوجي", - "MATH_IS_ODD": "هو فرذي", + "MATH_IS_ODD": "هو فردي", "MATH_IS_PRIME": "هو أولي", "MATH_IS_WHOLE": "هو صحيح", "MATH_IS_POSITIVE": "هو موجب", @@ -328,6 +327,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "القيام بشيء ما", "PROCEDURES_BEFORE_PARAMS": "مع:", "PROCEDURES_CALL_BEFORE_PARAMS": "مع:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "لا يمكن تشغيل الوظيفة المحددة من قبل المستخدم '%1' لأن كتلة التعريف معطلة.", "PROCEDURES_DEFNORETURN_TOOLTIP": "انشئ دالة بدون مخرجات .", "PROCEDURES_DEFNORETURN_COMMENT": "صف هذه الوظيفة...", "PROCEDURES_DEFRETURN_HELPURL": "https://tr.wikipedia.org/wiki/دالة_(برمجة)", diff --git a/msg/json/az.json b/msg/json/az.json index edf7ff7620b..b179b6a10bd 100644 --- a/msg/json/az.json +++ b/msg/json/az.json @@ -5,12 +5,14 @@ "Adil", "Cekli829", "Masalli qasimli", - "Şeyx Şamil" + "Şeyx Şamil", + "Əkrəm", + "Əkrəm Cəfər" ] }, "VARIABLES_DEFAULT_NAME": "element", "UNNAMED_KEY": "adsız", - "TODAY": "Bugün", + "TODAY": "Bu gün", "DUPLICATE_BLOCK": "Dublikat", "ADD_COMMENT": "Şərh əlavə et", "REMOVE_COMMENT": "Şərhi sil", @@ -220,8 +222,8 @@ "TEXT_TRIM_OPERATOR_RIGHT": "boşluqları yalnız sağ tərəfdən pozun", "TEXT_PRINT_TITLE": "%1 - i çap elə", "TEXT_PRINT_TOOLTIP": "Təyin olunmuş mətn, ədəd və ya hər hansı bir başqa elementi çap elə.", - "TEXT_PROMPT_TYPE_TEXT": "İstifadəçiyə mətn daxil etməsi üçün sorğunu/tələbi ismarıc ilə göndərin", - "TEXT_PROMPT_TYPE_NUMBER": "İstifadəçiyə ədəd daxil etməsi üçün sorğunu/tələbi ismarıc kimi göndərin", + "TEXT_PROMPT_TYPE_TEXT": "mesaj ehtiva edən mətn sorğusu", + "TEXT_PROMPT_TYPE_NUMBER": "mesaj ehtiva edən ədəd sorğusu", "TEXT_PROMPT_TOOLTIP_NUMBER": "İstifadəçiyə ədəd daxil etməsi üçün sorğu/tələb göndərin.", "TEXT_PROMPT_TOOLTIP_TEXT": "İstifadəçiyə mətn daxil etməsi üçün sorğu/tələb göndərin.", "TEXT_COUNT_MESSAGE0": "%2 içindən %1 sayını hesabla", diff --git a/msg/json/be-tarask.json b/msg/json/be-tarask.json index 6d5e0ff1913..b3959de94a4 100644 --- a/msg/json/be-tarask.json +++ b/msg/json/be-tarask.json @@ -10,15 +10,15 @@ "VARIABLES_DEFAULT_NAME": "аб’ект", "UNNAMED_KEY": "безназоўны", "TODAY": "Сёньня", - "DUPLICATE_BLOCK": "Капіяваць", + "DUPLICATE_BLOCK": "Дубляваць", "ADD_COMMENT": "Дадаць камэнтар", "REMOVE_COMMENT": "Выдаліць камэнтар", "DUPLICATE_COMMENT": "Прадубляваць камэнтар", "EXTERNAL_INPUTS": "Зьнешнія ўваходы", - "INLINE_INPUTS": "Унутраныя ўваходы", + "INLINE_INPUTS": "Убудаваныя ўваходы", "DELETE_BLOCK": "Выдаліць блёк", "DELETE_X_BLOCKS": "Выдаліць %1 блёкі", - "DELETE_ALL_BLOCKS": "Выдаліць усе блёкі %1?", + "DELETE_ALL_BLOCKS": "Выдаліць усе %1 блёкі?", "CLEAN_UP": "Ачысьціць блёкі", "COLLAPSE_BLOCK": "Згарнуць блёк", "COLLAPSE_ALL": "Згарнуць блёкі", @@ -31,11 +31,11 @@ "REDO": "Паўтарыць", "CHANGE_VALUE_TITLE": "Зьмяніць значэньне:", "RENAME_VARIABLE": "Перайменаваць зьменную…", - "RENAME_VARIABLE_TITLE": "Перайменаваць усе назвы зьменных '%1' на:", + "RENAME_VARIABLE_TITLE": "Перайменаваць усе зьменныя '%1' на:", "NEW_VARIABLE": "Стварыць зьменную…", "NEW_STRING_VARIABLE": "Стварыць радковую зьменную…", "NEW_NUMBER_VARIABLE": "Стварыць лікавую зьменную…", - "NEW_COLOUR_VARIABLE": "Стварыць зьменную колеру…", + "NEW_COLOUR_VARIABLE": "Стварыць колерную зьменную…", "NEW_VARIABLE_TYPE_TITLE": "Новы тып зьменнай:", "NEW_VARIABLE_TITLE": "Імя новай зьменнай:", "VARIABLE_ALREADY_EXISTS": "Зьменная з назвай «%1» ужо існуе.", @@ -310,6 +310,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "зрабіць што-небудзь", "PROCEDURES_BEFORE_PARAMS": "з:", "PROCEDURES_CALL_BEFORE_PARAMS": "з:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Немагчыма запусьціць карыстальніцкую функцыю '%1', бо адключаны блёк вызначэньня.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Стварае функцыю бяз выніку.", "PROCEDURES_DEFNORETURN_COMMENT": "Апішыце гэтую функцыю…", "PROCEDURES_DEFRETURN_RETURN": "вярнуць", diff --git a/msg/json/be.json b/msg/json/be.json index 3b4954ed16e..a7e26cb7902 100644 --- a/msg/json/be.json +++ b/msg/json/be.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Andoti", "No Sleep till Krupki", "Plaga med", "SimondR", @@ -8,6 +9,7 @@ ] }, "VARIABLES_DEFAULT_NAME": "элемент", + "UNNAMED_KEY": "неназваны", "TODAY": "Сёння", "DUPLICATE_BLOCK": "Дубляваць", "ADD_COMMENT": "Дадаць каментарый", diff --git a/msg/json/bg.json b/msg/json/bg.json index 66cfde41a99..29f4d78bec5 100644 --- a/msg/json/bg.json +++ b/msg/json/bg.json @@ -4,6 +4,7 @@ "Alpinistbg", "Gkostov", "InsomniHat", + "MTongov", "Miroslav35232", "NikiTricky", "ShockD", @@ -153,7 +154,7 @@ "MATH_IS_TOOLTIP": "Проверете дали дадено число е четно, нечетно, просто, цяло, положително, отрицателно или дали се дели на друго число. Връща вярно или невярно.", "MATH_CHANGE_HELPURL": "https://bg.wikipedia.org/wiki/Събиране", "MATH_CHANGE_TITLE": "промени %1 на %2", - "MATH_CHANGE_TOOLTIP": "Добави число към променлива „%1“.", + "MATH_CHANGE_TOOLTIP": "Добави число към променлива '%1'.", "MATH_ROUND_TOOLTIP": "Закръгли число нагоре или надолу.", "MATH_ROUND_OPERATOR_ROUND": "закръгли", "MATH_ROUND_OPERATOR_ROUNDUP": "закръгли нагоре", @@ -292,7 +293,7 @@ "LISTS_GET_SUBLIST_START_FROM_START": "вземи подсписък от №", "LISTS_GET_SUBLIST_START_FROM_END": "вземи подсписък от № от края", "LISTS_GET_SUBLIST_START_FIRST": "вземи подсписък от първия", - "LISTS_GET_SUBLIST_END_FROM_START": "до №", + "LISTS_GET_SUBLIST_END_FROM_START": "до #", "LISTS_GET_SUBLIST_END_FROM_END": "до № открая", "LISTS_GET_SUBLIST_END_LAST": "до края", "LISTS_GET_SUBLIST_TOOLTIP": "Копира част от списък.", @@ -319,6 +320,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "направиш", "PROCEDURES_BEFORE_PARAMS": "с:", "PROCEDURES_CALL_BEFORE_PARAMS": "с:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Не може да се изпълни дефинираната от потребителя функция '%1', защото дефиниращият блок е деактивиран.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Създава функция, която не връща резултат.", "PROCEDURES_DEFNORETURN_COMMENT": "Опишете тази функция...", "PROCEDURES_DEFRETURN_RETURN": "върни", diff --git a/msg/json/ce.json b/msg/json/ce.json index a6937aa748f..af9efea029a 100644 --- a/msg/json/ce.json +++ b/msg/json/ce.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Исмаил Садуев", "Саид Мисарбиев", "Умар" ] @@ -173,20 +174,20 @@ "MATH_ATAN2_TOOLTIP": "Йухайерхайо меттиг (X, Y) арктангенс градусашкахь -180 тӀера 180 кхаччалц.", "TEXT_TEXT_TOOLTIP": "Элп, дош, могӀам йозанехь.", "TEXT_JOIN_TITLE_CREATEWITH": "кхолла могӀам", - "TEXT_JOIN_TOOLTIP": "Вовшах тосу муьлхха дакъалган терахь, кхуллу йозанан кийсиг", + "TEXT_JOIN_TOOLTIP": "Вовшахтосу муьлха дакъалган терахь, кхуллу текстан кийсиг", "TEXT_CREATE_JOIN_TITLE_JOIN": "вовшахтаса", "TEXT_CREATE_JOIN_TOOLTIP": "ТӀетоха, дӀайаккха, дехьайаккха блок йухайан дакъалгаш.", - "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "ТӀетоха йозанан дакъалг", - "TEXT_APPEND_TITLE": "%1 тӀетоха йоза %2", - "TEXT_APPEND_TOOLTIP": "ТӀетоха йоза хийцалучунна \"%1\"", + "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "ТӀетоха текстан дакъалг", + "TEXT_APPEND_TITLE": "%1 тӀетоха текст %2", + "TEXT_APPEND_TOOLTIP": "ТӀетоха текст хийцалучунна \"%1\"", "TEXT_LENGTH_TITLE": "дохалла %1", "TEXT_LENGTH_TOOLTIP": "Йухадерзадо сийлаллин терахь деллачу йозанехь", "TEXT_ISEMPTY_TITLE": "%1 деса ду", - "TEXT_ISEMPTY_TOOLTIP": "Нагахь йоза деса делахь, йухадерзадо бакъ долу маьӀна.", - "TEXT_INDEXOF_TOOLTIP": "Йухайерзайо лоьмар хьалхара/тӀаьххьара хьалхара йоза шолгӀачун чуваларан меттиган. Йухайерзайо %1, нагахь йоза ца карийнехь.", + "TEXT_ISEMPTY_TOOLTIP": "Нагахь текст йаьсса йелахь, йухадерзадо бакъ долу маьӀна.", + "TEXT_INDEXOF_TOOLTIP": "Йухайерзайо позицин лоьмар хьалхара/тӀаьххьара хьалхара текст шолгӀачун йукъа йахар. Йухайерзайо %1, нагахь текст ца карийнехь.", "TEXT_INDEXOF_TITLE": "Йозанехь %1, %2, %3", "TEXT_INDEXOF_OPERATOR_FIRST": "Караде хьалхара йозанера чувалар", - "TEXT_INDEXOF_OPERATOR_LAST": "караде тӀаьххьара чувалар йозанан", + "TEXT_INDEXOF_OPERATOR_LAST": "караде тӀаьххьара текстан йукъайар", "TEXT_CHARAT_TITLE": "Йозанехь %1, %2", "TEXT_CHARAT_FROM_START": "№ йолу элп схьаэца", "TEXT_CHARAT_FROM_END": "№ йолу элп схьаэца тӀехььара", @@ -194,7 +195,7 @@ "TEXT_CHARAT_LAST": "тӀаьххьара элп схьаэца", "TEXT_CHARAT_RANDOM": "Схаэца ца хоржуш элп", "TEXT_CHARAT_TOOLTIP": "Йухадерзадо элп гайтинчу меттиге", - "TEXT_GET_SUBSTRING_TOOLTIP": "Йозанан гайтина дакъа йухадерзадо", + "TEXT_GET_SUBSTRING_TOOLTIP": "Текстан гайтина дакъа йухадерзадо", "TEXT_GET_SUBSTRING_INPUT_IN_TEXT": "Йозанехь", "TEXT_GET_SUBSTRING_START_FROM_START": "Схьаэца бухара могӀа № йолу элпера", "TEXT_GET_SUBSTRING_START_FROM_END": "Схьаэца бухара могӀа № йолу элпера тӀехьара", @@ -202,22 +203,22 @@ "TEXT_GET_SUBSTRING_END_FROM_START": "№ йолу элп схьаэца", "TEXT_GET_SUBSTRING_END_FROM_END": "№ йолу элп схьаэца тӀехьара", "TEXT_GET_SUBSTRING_END_LAST": "тӀаьххьара элп схьаэца", - "TEXT_CHANGECASE_TOOLTIP": "Йухайерзайо йозанан копи ХЬАЛХАРЧУ йа могӀанан элпашца", + "TEXT_CHANGECASE_TOOLTIP": "Йухайерзайо текстан копи ХЬАЛХАРЧУ йа могӀанан элпашца", "TEXT_CHANGECASE_OPERATOR_UPPERCASE": "ХЬАЛХАРЧУ ЭЛПАШКА", "TEXT_CHANGECASE_OPERATOR_LOWERCASE": "могӀанан элпаш", "TEXT_CHANGECASE_OPERATOR_TITLECASE": "Хьалхарчу Коьрта Элпашка", - "TEXT_TRIM_TOOLTIP": "Йухайерзайо йозанан копи генайаьккхина йукъ цхьаьна йа шинне а йуьххьера", + "TEXT_TRIM_TOOLTIP": "Йухайерзайо текстан копи генайаьккхина йукъ цхьаьна йа шинне а йуьххьера", "TEXT_TRIM_OPERATOR_BOTH": "Йаккъаш дӀахедайар", "TEXT_TRIM_OPERATOR_LEFT": "аьрру агӀор йукъ дӀахадайар", "TEXT_TRIM_OPERATOR_RIGHT": "Аьтту агӀор йаккъаш дӀахедайе", "TEXT_PRINT_TITLE": "Зорбатоха %1", - "TEXT_PRINT_TOOLTIP": "Зорбатуху йозанан, терахьан йа кхечу объектан", - "TEXT_PROMPT_TYPE_TEXT": "йоза деха дӀааларца", + "TEXT_PRINT_TOOLTIP": "Зорбатуху текстан, терахьан йа кхечу объектан", + "TEXT_PROMPT_TYPE_TEXT": "текст деха дӀааларца", "TEXT_PROMPT_TYPE_NUMBER": "терахь деха дӀааларца", "TEXT_PROMPT_TOOLTIP_NUMBER": "Лелочунга терахь деха.", - "TEXT_PROMPT_TOOLTIP_TEXT": "Йоза деха лелочуьнга.", + "TEXT_PROMPT_TOOLTIP_TEXT": "Текст йеха декъашхочунга.", "TEXT_COUNT_MESSAGE0": "Дагарде барам %1 %2", - "TEXT_COUNT_TOOLTIP": "Дагарде, мосазза йолу йозанан кийсиг кхечу йозанехь.", + "TEXT_COUNT_TOOLTIP": "Дагарде, мосазза йолу текстан кийсиг кхечу текстехь гучуйолу.", "TEXT_REPLACE_MESSAGE0": "хийца %1 %2 %3", "TEXT_REPLACE_TOOLTIP": "Хийца ша долу дерриг йозананна чуваларш, кхечу йозанца", "TEXT_REVERSE_MESSAGE0": "хийца низам йуханехьа %1", @@ -228,7 +229,7 @@ "LISTS_CREATE_WITH_INPUT_WITH": "кхолла могӀам", "LISTS_CREATE_WITH_CONTAINER_TITLE_ADD": "Исписка", "LISTS_CREATE_WITH_CONTAINER_TOOLTIP": "ТӀетоха, дӀайаккха, дехьайаккха блок йухайан дакъалгаш.", - "LISTS_CREATE_WITH_ITEM_TOOLTIP": "ТӀетоха йозанан дакъалг", + "LISTS_CREATE_WITH_ITEM_TOOLTIP": "ТӀетоха текстан дакъалг", "LISTS_REPEAT_TOOLTIP": "Лаьтташ йолу дакъалган копин терахьех исписка кхуллу.", "LISTS_REPEAT_TITLE": "кхолла дакъалг %1 исписка, йухайзйеш йолун %2", "LISTS_LENGTH_TITLE": "дохалла %1", @@ -237,8 +238,8 @@ "LISTS_ISEMPTY_TOOLTIP": "Нагахь исписка йеса йелахь, йухадерзадо бакъдолчун маьӀна", "LISTS_INLIST": "испискехь", "LISTS_INDEX_OF_FIRST": "Караде хьалхара йозанера чувалар", - "LISTS_INDEX_OF_LAST": "караде тӀаьххьара чувалар йозанан", - "LISTS_INDEX_OF_TOOLTIP": "Йухайерзайо лоьмар хьалхара/тӀаьххьара хьалхара йоза шолгӀачун чуваларан меттиган. Йухайерзайо %1, нагахь йоза ца карийнехь.", + "LISTS_INDEX_OF_LAST": "караде тӀаьххьара текстан йуъадар", + "LISTS_INDEX_OF_TOOLTIP": "Йухайерзайо позицин лоьмар хьалхара/тӀаьххьара могӀам чу элемент йахар. Йухайерзайо %1, нагахь элемент ца карийнехь.", "LISTS_GET_INDEX_GET": "Схьаэца", "LISTS_GET_INDEX_GET_REMOVE": "Схьаэца, дӀадаккха", "LISTS_GET_INDEX_REMOVE": "дӀайаккха", @@ -246,7 +247,7 @@ "LISTS_GET_INDEX_FROM_END": "№ тӀехьара", "LISTS_GET_INDEX_FIRST": "хьалхара", "LISTS_GET_INDEX_LAST": "тӀаьххьара", - "LISTS_GET_INDEX_RANDOM": "Нисделларг", + "LISTS_GET_INDEX_RANDOM": "ма-луъу", "LISTS_INDEX_FROM_START_TOOLTIP": "%1 - хьалхара дакъалг", "LISTS_INDEX_FROM_END_TOOLTIP": "%1 - тӀаьххьара дакъалг", "LISTS_GET_INDEX_TOOLTIP_GET_FROM": "Йухайерзайо дакъалг испискан гайтинчу меттиге.", @@ -287,10 +288,10 @@ "LISTS_SORT_TYPE_TEXT": "абатца", "LISTS_SORT_TYPE_IGNORECASE": "Абатца, регистран чот а ца лелош", "LISTS_SPLIT_LIST_FROM_TEXT": "йозанах исписка йар", - "LISTS_SPLIT_TEXT_FROM_LIST": "йоза вовшахтоха испискех", + "LISTS_SPLIT_TEXT_FROM_LIST": "текст вовшахтоха могӀанах", "LISTS_SPLIT_WITH_DELIMITER": "доькъучунца", - "LISTS_SPLIT_TOOLTIP_SPLIT": "Доькъу йоза йозанан испискан доькъучуьнца", - "LISTS_SPLIT_TOOLTIP_JOIN": "Вовшахтуху йозанийн исписка цхьаьна йозане доькъучуьнца.", + "LISTS_SPLIT_TOOLTIP_SPLIT": "Йоькъу текст могӀаман текст тӀе доькъучуьнца", + "LISTS_SPLIT_TOOLTIP_JOIN": "Вовшахтуху текстийн могӀам цхьаьна тексте доькъучуьнца.", "LISTS_REVERSE_MESSAGE0": "хийца низам йуханехьа %1", "LISTS_REVERSE_TOOLTIP": "Испискан низам йуханехьа хийца", "VARIABLES_GET_TOOLTIP": "Оцу хийцалучун маьӀна хуьйцу", @@ -302,6 +303,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "кхочушдан цхьа хӀума", "PROCEDURES_BEFORE_PARAMS": "дуьйна:", "PROCEDURES_CALL_BEFORE_PARAMS": "дуьйна:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Декъашхочун '%1' функци дӀайахьа йиш йац, хӀунда аьлча билгалйаьккхина блок дӀайаьккхина йу.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Кхуллу маьӀна йуха ца дерзош йолу процедура", "PROCEDURES_DEFNORETURN_COMMENT": "Гайта и функци", "PROCEDURES_DEFRETURN_RETURN": "йухадерзо", diff --git a/msg/json/constants.json b/msg/json/constants.json index 2677e406f74..ede1c1b75c2 100644 --- a/msg/json/constants.json +++ b/msg/json/constants.json @@ -1,12 +1,11 @@ { - "#": "Automatically generated, do not edit this file!", - "COLOUR_HUE": "20", + "MATH_HUE": "230", + "LOOPS_HUE": "120", "LISTS_HUE": "260", "LOGIC_HUE": "210", - "LOOPS_HUE": "120", - "MATH_HUE": "230", - "PROCEDURES_HUE": "290", + "VARIABLES_HUE": "330", "TEXTS_HUE": "160", - "VARIABLES_DYNAMIC_HUE": "310", - "VARIABLES_HUE": "330" -} \ No newline at end of file + "PROCEDURES_HUE": "290", + "COLOUR_HUE": "20", + "VARIABLES_DYNAMIC_HUE": "310" +} diff --git a/msg/json/cs.json b/msg/json/cs.json index 1855d9752c6..ca17fa91eb0 100644 --- a/msg/json/cs.json +++ b/msg/json/cs.json @@ -1,7 +1,6 @@ { "@metadata": { "authors": [ - "Amire80", "Chmee2", "Clon", "Dita", @@ -12,6 +11,7 @@ "Ilimanaq29", "Koo6", "Matěj Grabovský", + "Mormegil", "Patriccck", "Patrik L.", "Robins7", @@ -53,6 +53,7 @@ "NEW_VARIABLE_TITLE": "Nový název proměnné:", "VARIABLE_ALREADY_EXISTS": "Proměnná jménem '%1' již existuje", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Proměnná pojmenovaná jako '%1' již existuje pro jiný typ: '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Proměnná pojmenovaná „%1“ již jako parametr v proceduře „%2“ existuje.", "DELETE_VARIABLE_CONFIRMATION": "Odstranit %1 použití proměnné '%2'", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Proměnnou '%1' není možné odstranit, protože je součástí definice funkce '%2'", "DELETE_VARIABLE": "Odstranit proměnnou '%1'", diff --git a/msg/json/de.json b/msg/json/de.json index 33ed7191066..914be827d3e 100644 --- a/msg/json/de.json +++ b/msg/json/de.json @@ -1,7 +1,6 @@ { "@metadata": { "authors": [ - "Amire80", "Brettchenweber", "Cvanca", "Dan-yell", @@ -110,7 +109,7 @@ "LOGIC_COMPARE_TOOLTIP_GTE": "Ist wahr, falls der erste Wert größer als oder gleich groß wie der zweite Wert ist.", "LOGIC_OPERATION_TOOLTIP_AND": "Ist wahr, falls beide Werte wahr sind.", "LOGIC_OPERATION_AND": "und", - "LOGIC_OPERATION_TOOLTIP_OR": "Ist wahr, falls mindestens einer der beiden Werte wahr ist.", + "LOGIC_OPERATION_TOOLTIP_OR": "Gibt true zurück, wenn mindestens eine der Eingaben wahr ist.", "LOGIC_OPERATION_OR": "oder", "LOGIC_NEGATE_TITLE": "nicht %1", "LOGIC_NEGATE_TOOLTIP": "Ist wahr, falls der Eingabewert unwahr ist. Ist unwahr, falls der Eingabewert wahr ist.", @@ -342,6 +341,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "etwas tun", "PROCEDURES_BEFORE_PARAMS": "mit:", "PROCEDURES_CALL_BEFORE_PARAMS": "mit:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Die benutzerdefinierte Funktion '%1' kann nicht ausgeführt werden, weil der Definitionsblock deaktiviert ist.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Ein Funktionsblock ohne Rückgabewert.", "PROCEDURES_DEFNORETURN_COMMENT": "Beschreibe diese Funktion …", "PROCEDURES_DEFRETURN_HELPURL": "https://de.wikipedia.org/wiki/Prozedur_(Programmierung)", diff --git a/msg/json/diq.json b/msg/json/diq.json index bc37d442a9c..9661e9c0c0b 100644 --- a/msg/json/diq.json +++ b/msg/json/diq.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "1917 Ekim Devrimi", + "GolyatGeri", "Gırd", "Kumkumuk", "Marmase", @@ -77,7 +78,9 @@ "CONTROLS_IF_MSG_ELSEIF": "eke nêyo", "CONTROLS_IF_MSG_ELSE": "eke çıniyo", "CONTROLS_IF_ELSEIF_TOOLTIP": "Bloq da if'i rê yu şert dekerê de.", + "CONTROLS_IF_ELSE_TOOLTIP": "Blokê if'i rê yew şerto ke her çi ihtıwa keno, cıkerê.", "LOGIC_COMPARE_TOOLTIP_EQ": "Debiyaye dı erci zey pêyêse ercê \"True\" dane.", + "LOGIC_COMPARE_TOOLTIP_NEQ": "Dı cıbiyayeyi ke zey yewbini niyê, bıçarne era raşt.", "LOGIC_OPERATION_TOOLTIP_AND": "Eger her dı cıkewtışi zi raştê, şıma ageyrê.", "LOGIC_OPERATION_AND": "û", "LOGIC_OPERATION_OR": "ya zi", @@ -129,11 +132,13 @@ "MATH_IS_NEGATIVE": "negatifo", "MATH_IS_DIVISIBLE_BY": "Leteyêno", "MATH_CHANGE_TITLE": "%2, keno %1 vurneno", + "MATH_CHANGE_TOOLTIP": "Yew amari be '%1' vurriyayoğ ra zêde ke.", "MATH_ROUND_TOOLTIP": "Yu amorer loğê cêri yana cori ke", "MATH_ROUND_OPERATOR_ROUND": "gılor ke", "MATH_ROUND_OPERATOR_ROUNDUP": "Loğê cori ke", "MATH_ROUND_OPERATOR_ROUNDDOWN": "Loğê cêri ke", "MATH_ONLIST_OPERATOR_SUM": "koma liste", + "MATH_ONLIST_TOOLTIP_SUM": "Antolociya heme amaran liste de açarne.", "MATH_ONLIST_OPERATOR_MIN": "Tewr qıcê lista", "MATH_ONLIST_TOOLTIP_MIN": "Lista de tewr qıckek amar tadê", "MATH_ONLIST_OPERATOR_MAX": "Tewr gırdê lista", @@ -151,7 +156,7 @@ "MATH_ATAN2_HELPURL": "https://diq.wikipedia.org/wiki/Atan2", "MATH_ATAN2_TITLE": "atan2, X:%1 Y:%2", "TEXT_TEXT_TOOLTIP": "Yu herfa, satır yana çekuya metini", - "TEXT_JOIN_TITLE_CREATEWITH": "ya metin vıraz", + "TEXT_JOIN_TITLE_CREATEWITH": "metın vıraze", "TEXT_CREATE_JOIN_TITLE_JOIN": "gıre de", "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "Yew işaret nuşteyi ke.", "TEXT_APPEND_TITLE": "rê %1 Metin dek %2", @@ -229,7 +234,7 @@ "LISTS_SORT_TYPE_TEXT": "Alfabetik", "LISTS_SORT_TYPE_IGNORECASE": "alfabetik, nezeri mekerên", "LISTS_SPLIT_LIST_FROM_TEXT": "metini ra lista bıkerê", - "LISTS_SPLIT_TEXT_FROM_LIST": "Lista ra metin bıkerê", + "LISTS_SPLIT_TEXT_FROM_LIST": "lista ra metın vıraze", "LISTS_SPLIT_WITH_DELIMITER": "Hududoxi ya", "LISTS_REVERSE_MESSAGE0": "%1 dimlaşt kerê", "LISTS_REVERSE_TOOLTIP": "Yew kopyaya yew lista dimlaşt kerê.", @@ -253,6 +258,7 @@ "PROCEDURES_CREATE_DO": "'%1' vıraze", "WORKSPACE_COMMENT_DEFAULT_TEXT": "Çiyê vace...", "WORKSPACE_ARIA_LABEL": "Blockly Caygurenayışi", + "COLLAPSED_WARNINGS_WARNING": "Blokê xırabeyi iqazan ihtıwa kenê.", "DIALOG_OK": "TEMAM", "DIALOG_CANCEL": "Bıtexelne" } diff --git a/msg/json/es.json b/msg/json/es.json index 66118d46dad..309750f3f13 100644 --- a/msg/json/es.json +++ b/msg/json/es.json @@ -7,7 +7,9 @@ "Eulalio", "Fitoschido", "Harvest", + "Ignatgg", "Indiralena", + "Josuert", "Julián L", "Ktranz", "Luisangelrg", @@ -332,6 +334,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "hacer algo", "PROCEDURES_BEFORE_PARAMS": "con:", "PROCEDURES_CALL_BEFORE_PARAMS": "con:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "No se puede ejecutar la función definida por el usuario '%1' porque el bloque de definición está deshabilitado.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Crea una función sin salida.", "PROCEDURES_DEFNORETURN_COMMENT": "Describe esta función...", "PROCEDURES_DEFRETURN_RETURN": "devuelve", diff --git a/msg/json/et.json b/msg/json/et.json index 91af281c567..16f4d0e00cb 100644 --- a/msg/json/et.json +++ b/msg/json/et.json @@ -5,6 +5,7 @@ "Hasso", "Ilmarine", "Masavi", + "Pajusmar", "Puik" ] }, @@ -41,6 +42,7 @@ "NEW_VARIABLE_TITLE": "Uue muutuja nimi:", "VARIABLE_ALREADY_EXISTS": "'%1'-nimeline muutuja on juba olemas.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Muutuja nimega '%1' juba eksisteerib teise tüübina: '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Muutuja nimega '%1' on protseduuris '%2' juba parameetrina olemas.", "DELETE_VARIABLE_CONFIRMATION": "Kas kustutada %1 kohas kasutatav muutuja '%2'?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Muutujat '%1' ei saa kustutada, sest see on osa funktsiooni '%2' määratlusest", "DELETE_VARIABLE": "Kustuta muutuja '%1'", @@ -106,6 +108,12 @@ "LOGIC_TERNARY_TOOLTIP": "Kui tingimuse väärtus on tõene, tagastab „kui tõene“ väärtuse, vastasel juhul „kui väär“ väärtuse.", "MATH_NUMBER_HELPURL": "https://et.wikipedia.org/wiki/Arv", "MATH_NUMBER_TOOLTIP": "Arv.", + "MATH_TRIG_SIN": "sin", + "MATH_TRIG_COS": "cos", + "MATH_TRIG_TAN": "tan", + "MATH_TRIG_ASIN": "asin", + "MATH_TRIG_ACOS": "acos", + "MATH_TRIG_ATAN": "atan", "MATH_ARITHMETIC_HELPURL": "https://et.wikipedia.org/wiki/Aritmeetika", "MATH_ARITHMETIC_TOOLTIP_ADD": "Tagastab kahe arvu summa.", "MATH_ARITHMETIC_TOOLTIP_MINUS": "Tagastab kahe arvu vahe.", @@ -300,6 +308,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "teeme midagi", "PROCEDURES_BEFORE_PARAMS": "sisenditega:", "PROCEDURES_CALL_BEFORE_PARAMS": "sisenditega:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Kasutaja määratud funktsiooni '%1' ei saa käivitada, kuna määratlusplokk on keelatud.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Tekitab funktsiooni, mis ei tagasta midagi.", "PROCEDURES_DEFNORETURN_COMMENT": "Funktsiooni kirjeldus ...", "PROCEDURES_DEFRETURN_RETURN": "tagasta", diff --git a/msg/json/fa.json b/msg/json/fa.json index a27d412ab45..d839853165e 100644 --- a/msg/json/fa.json +++ b/msg/json/fa.json @@ -7,6 +7,7 @@ "AzorAhai", "Dalba", "Darafsh", + "Ebrahim", "Ebraminio", "Hamisun", "Hossein.safavi", diff --git a/msg/json/fr.json b/msg/json/fr.json index 4d6b9894ca6..eadac77e7ec 100644 --- a/msg/json/fr.json +++ b/msg/json/fr.json @@ -84,7 +84,7 @@ "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "Tant qu’une valeur est vraie, alors exécuter des instructions.", "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "Tant qu’une valeur est fausse, alors exécuter des instructions.", "CONTROLS_FOR_HELPURL": "https://fr.wikipedia.org/wiki/Boucle_for", - "CONTROLS_FOR_TOOLTIP": "Faire prendre successivement à la variable « %1 » les valeurs entre deux nombres de début et de fin par incrément du pas spécifié et exécuter les instructions spécifiées.", + "CONTROLS_FOR_TOOLTIP": "Faire prendre successivement à la variable « %1 » les valeurs entre le nombre du début et celui de fin en incrémentant du pas spécifié et exécuter les instructions spécifiées.", "CONTROLS_FOR_TITLE": "compter avec %1 de %2 à %3 par %4", "CONTROLS_FOREACH_HELPURL": "https://fr.wikipedia.org/wiki/Structure_de_contrôle#Itérateurs", "CONTROLS_FOREACH_TITLE": "pour chaque élément %1 dans la liste %2", @@ -346,6 +346,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "faire quelque chose", "PROCEDURES_BEFORE_PARAMS": "avec :", "PROCEDURES_CALL_BEFORE_PARAMS": "avec :", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Impossible d'exécuter la fonction définie par l'utilisateur '%1' car le bloc de définition est désactivé.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Crée une fonction sans sortie.", "PROCEDURES_DEFNORETURN_COMMENT": "Décrivez cette fonction...", "PROCEDURES_DEFRETURN_HELPURL": "https://fr.wikipedia.org/wiki/Sous-programme", diff --git a/msg/json/he.json b/msg/json/he.json index c5db9108e3d..954a9f03f00 100644 --- a/msg/json/he.json +++ b/msg/json/he.json @@ -64,7 +64,7 @@ "COLOUR_RGB_RED": "אדום", "COLOUR_RGB_GREEN": "ירוק", "COLOUR_RGB_BLUE": "כחול", - "COLOUR_RGB_TOOLTIP": "צור צבע עם הסכום המצוין של אדום, ירוק וכחול. כל הערכים חייבים להיות בין 0 ל100.", + "COLOUR_RGB_TOOLTIP": "צור צבע עם הסכום המצוין של אדום, ירוק וכחול. כל הערכים חייבים להיות בין 0 ל־100.", "COLOUR_BLEND_TITLE": "ערבב", "COLOUR_BLEND_COLOUR1": "צבע 1", "COLOUR_BLEND_COLOUR2": "צבע 2", @@ -328,6 +328,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "לעשות משהו", "PROCEDURES_BEFORE_PARAMS": "עם:", "PROCEDURES_CALL_BEFORE_PARAMS": "עם:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "אי־אפשר להריץ את הפונקציה „%1” שהוגדרה על־ידי המשתמש כיוון שמקטע ההגדרה מושבת.", "PROCEDURES_DEFNORETURN_TOOLTIP": "יצירת פונקציה ללא פלט.", "PROCEDURES_DEFNORETURN_COMMENT": "תאר את הפונקציה הזאת...", "PROCEDURES_DEFRETURN_RETURN": "להחזיר", diff --git a/msg/json/hr.json b/msg/json/hr.json index 500f8395af8..22831784578 100644 --- a/msg/json/hr.json +++ b/msg/json/hr.json @@ -4,6 +4,7 @@ "Bugoslav", "Gordana Sokol", "Lkralj15", + "N0one", "Ninocka", "Npavcec", "Tjagust" @@ -42,6 +43,7 @@ "NEW_VARIABLE_TITLE": "Ime nove varijable:", "VARIABLE_ALREADY_EXISTS": "Varijabla s nazivom '%1' već postoji.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Varijabla pod nazivom '%1' već postoji za drugi tip: '%2'", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Varijabla pod nazivom '%1' već postoji kao parametar u proceduri '%2'.", "DELETE_VARIABLE_CONFIRMATION": "Obriši %1 korištenja varijable '%2'?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Ne mogu obrisati varijablu '%1' zato što je dio definicije funkcije '%2'", "DELETE_VARIABLE": "Obriši varijablu '%1'", @@ -109,6 +111,12 @@ "LOGIC_TERNARY_TOOLTIP": "Provjerite uvjet u \"izrazu\". Ako je uvjet istinit, vraća vrijednost \"ako je istinito\"; inače vraća vrijednost \"ako je lažno\".", "MATH_NUMBER_HELPURL": "https://hr.wikipedia.org/wiki/Broj", "MATH_NUMBER_TOOLTIP": "broj", + "MATH_TRIG_SIN": "sin", + "MATH_TRIG_COS": "cos", + "MATH_TRIG_TAN": "tan", + "MATH_TRIG_ASIN": "asin", + "MATH_TRIG_ACOS": "acos", + "MATH_TRIG_ATAN": "atan", "MATH_ARITHMETIC_HELPURL": "https://hr.wikipedia.org/wiki/Aritmetika", "MATH_ARITHMETIC_TOOLTIP_ADD": "Vraća zbroj dvaju brojeva.", "MATH_ARITHMETIC_TOOLTIP_MINUS": "Vraća razliku dvaju brojeva.", @@ -244,6 +252,7 @@ "LISTS_GET_INDEX_GET": "dohvati", "LISTS_GET_INDEX_GET_REMOVE": "uzmi i ukloni", "LISTS_GET_INDEX_REMOVE": "ukloni", + "LISTS_GET_INDEX_FROM_START": "#", "LISTS_GET_INDEX_FROM_END": "# od kraja", "LISTS_GET_INDEX_FIRST": "prvi", "LISTS_GET_INDEX_LAST": "posljednji", @@ -303,6 +312,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "napravi nešto", "PROCEDURES_BEFORE_PARAMS": "s:", "PROCEDURES_CALL_BEFORE_PARAMS": "s:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Nije moguće pokrenuti korisnički definiranu funkciju '%1' jer je blok definicije onemogućen.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Stvaranje funkcije bez izlazne vrijednosti", "PROCEDURES_DEFNORETURN_COMMENT": "Opis funkcije", "PROCEDURES_DEFRETURN_RETURN": "Vrati", diff --git a/msg/json/ia.json b/msg/json/ia.json index f7600660271..6cc26f25d53 100644 --- a/msg/json/ia.json +++ b/msg/json/ia.json @@ -14,8 +14,8 @@ "ADD_COMMENT": "Adder commento", "REMOVE_COMMENT": "Remover commento", "DUPLICATE_COMMENT": "Duplicar commento", - "EXTERNAL_INPUTS": "Entrata externe", - "INLINE_INPUTS": "Entrata interne", + "EXTERNAL_INPUTS": "Entratas externe", + "INLINE_INPUTS": "Entratas interne", "DELETE_BLOCK": "Deler bloco", "DELETE_X_BLOCKS": "Deler %1 blocos", "DELETE_ALL_BLOCKS": "Deler tote le %1 blocos?", diff --git a/msg/json/id.json b/msg/json/id.json index bea468f98af..49da77c08a2 100644 --- a/msg/json/id.json +++ b/msg/json/id.json @@ -5,10 +5,12 @@ "Akmaie Ajam", "Arifin.wijaya", "Daud I.F. Argana", + "ID Owly01", "Kasimtan", "Kenrick95", "Marwan Mohamad", "Mirws", + "NikolasKHF", "PutriAmalia1991", "Veracious", "아라" @@ -22,7 +24,7 @@ "REMOVE_COMMENT": "Hapus Komentar", "DUPLICATE_COMMENT": "Gandakan Komentar", "EXTERNAL_INPUTS": "Input Eksternal", - "INLINE_INPUTS": "Input Inline", + "INLINE_INPUTS": "Input Sebaris", "DELETE_BLOCK": "Hapus Blok", "DELETE_X_BLOCKS": "Hapus %1 Blok", "DELETE_ALL_BLOCKS": "Hapus semua %1 blok?", @@ -41,7 +43,7 @@ "RENAME_VARIABLE_TITLE": "Ubah nama semua variabel '%1' menjadi:", "NEW_VARIABLE": "Buat variabel...", "NEW_STRING_VARIABLE": "Buat variabel string...", - "NEW_NUMBER_VARIABLE": "Buat variabel number...", + "NEW_NUMBER_VARIABLE": "Buat variabel bilangan...", "NEW_COLOUR_VARIABLE": "Buat variabel warna...", "NEW_VARIABLE_TYPE_TITLE": "Tipe variabel baru:", "NEW_VARIABLE_TITLE": "Nama variabel baru:", @@ -311,6 +313,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "buat sesuatu", "PROCEDURES_BEFORE_PARAMS": "dengan:", "PROCEDURES_CALL_BEFORE_PARAMS": "dengan:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Tidak dapat menjalankan fungsi yang ditentukan pengguna '%1' karena blok definisi dinonaktifkan.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Buat sebuah fungsi tanpa output.", "PROCEDURES_DEFNORETURN_COMMENT": "Jelaskan fungsi ini...", "PROCEDURES_DEFRETURN_RETURN": "kembali", diff --git a/msg/json/ja.json b/msg/json/ja.json index 0cee86da959..3fcd1bd844f 100644 --- a/msg/json/ja.json +++ b/msg/json/ja.json @@ -335,6 +335,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "何かする", "PROCEDURES_BEFORE_PARAMS": "引数:", "PROCEDURES_CALL_BEFORE_PARAMS": "引数:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "定義ブロックが無効のため、ユーザー定義関数 '%1' を実行できません。", "PROCEDURES_DEFNORETURN_TOOLTIP": "出力なしの関数を作成します。", "PROCEDURES_DEFNORETURN_COMMENT": "この関数の説明…", "PROCEDURES_DEFRETURN_RETURN": "返す", diff --git a/msg/json/kn.json b/msg/json/kn.json index b811ca90c0e..1c1cdaf819e 100644 --- a/msg/json/kn.json +++ b/msg/json/kn.json @@ -3,6 +3,7 @@ "authors": [ "Ananth subray", "Anoop rao", + "Anzx", "Ashay vb", "Ksh31", "Ksramwiki1957", diff --git a/msg/json/ko.json b/msg/json/ko.json index bbfa5eb9488..142143422c6 100644 --- a/msg/json/ko.json +++ b/msg/json/ko.json @@ -2,7 +2,6 @@ "@metadata": { "authors": [ "Alex00728", - "Amire80", "Codenstory", "Delanoor", "Dr1t jg", @@ -21,6 +20,7 @@ "SeoJeongHo", "Snddl3", "Suleiman the Magnificent Television", + "YeBoy371", "Ykhwong", "아라" ] @@ -80,18 +80,18 @@ "CONTROLS_REPEAT_TITLE": "%1회 반복", "CONTROLS_REPEAT_INPUT_DO": "하기", "CONTROLS_REPEAT_TOOLTIP": "여러 번 반복해 명령들을 실행합니다.", - "CONTROLS_WHILEUNTIL_HELPURL": "https://ko.wikipedia.org/wiki/While_%EB%A3%A8%ED%94%84", + "CONTROLS_WHILEUNTIL_HELPURL": "https://github.com/google/blockly/wiki/Loops#repeat", "CONTROLS_WHILEUNTIL_OPERATOR_WHILE": "동안 반복", "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "다음까지 반복", "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "값이 참일 때, 명령들을 실행합니다.", "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "값이 거짓인 동안 명령문을 실행합니다.", - "CONTROLS_FOR_HELPURL": "https://ko.wikipedia.org/wiki/For_%EB%A3%A8%ED%94%84", + "CONTROLS_FOR_HELPURL": "https://github.com/google/blockly/wiki/Loops#count-with", "CONTROLS_FOR_TOOLTIP": "변수 '%1'이(가) 지정된 간격으로 시작 번호에서 끝 번호까지 세는 동시에 지정된 블록을 실행하게 하세요.", "CONTROLS_FOR_TITLE": "으로 계산 %1 %2에서 %4을 이용하여 %3로", - "CONTROLS_FOREACH_HELPURL": "https://ko.wikipedia.org/wiki/For_%EB%A3%A8%ED%94%84#.EC.9E.84.EC.9D.98.EC.9D.98_.EC.A7.91.ED.95.A9", + "CONTROLS_FOREACH_HELPURL": "https://github.com/google/blockly/wiki/Loops#for-each", "CONTROLS_FOREACH_TITLE": "각 항목에 대해 %1 목록으로 %2", "CONTROLS_FOREACH_TOOLTIP": "리스트 안에 들어있는 각 아이템들을, 순서대로 변수 '%1' 에 한 번씩 저장시키고, 그 때 마다 명령을 실행합니다.", - "CONTROLS_FLOW_STATEMENTS_HELPURL": "https://ko.wikipedia.org/wiki/%EC%A0%9C%EC%96%B4_%ED%9D%90%EB%A6%84", + "CONTROLS_FLOW_STATEMENTS_HELPURL": "https://github.com/google/blockly/wiki/Loops#loop-termination-blocks", "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "반복 중단", "CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE": "다음 반복", "CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK": "현재 반복 실행 블럭을 빠져나갑니다.", @@ -115,21 +115,22 @@ "LOGIC_COMPARE_TOOLTIP_LTE": "첫 번째 값이 두 번째 값보다 작거나 같으면, 참(true) 값을 돌려줍니다.", "LOGIC_COMPARE_TOOLTIP_GT": "첫 번째 값이 두 번째 값보다 크면, 참(true) 값을 돌려줍니다.", "LOGIC_COMPARE_TOOLTIP_GTE": "첫 번째 값이 두 번째 값보다 크거나 같으면, 참(true) 값을 돌려줍니다.", - "LOGIC_OPERATION_HELPURL": "https://ko.wikipedia.org/wiki/%EB%B6%88_%EB%85%BC%EB%A6%AC", + "LOGIC_OPERATION_HELPURL": "https://github.com/google/blockly/wiki/Logic#logical-operations", "LOGIC_OPERATION_TOOLTIP_AND": "두 값이 모두 참(true) 값이면, 참 값을 돌려줍니다.", "LOGIC_OPERATION_AND": "그리고", "LOGIC_OPERATION_TOOLTIP_OR": "적어도 하나의 값이 참일 경우 참을 반환합니다.", "LOGIC_OPERATION_OR": "또는", - "LOGIC_NEGATE_HELPURL": "https://ko.wikipedia.org/wiki/%EB%B6%80%EC%A0%95", + "LOGIC_NEGATE_HELPURL": "https://github.com/google/blockly/wiki/Logic#not", "LOGIC_NEGATE_TITLE": "%1가 아닙니다", "LOGIC_NEGATE_TOOLTIP": "입력값이 거짓이라면 참을 반환합니다. 참이라면 거짓을 반환합니다.", - "LOGIC_BOOLEAN_HELPURL": "https://ko.wikipedia.org/wiki/%EC%A7%84%EB%A6%BF%EA%B0%92", + "LOGIC_BOOLEAN_HELPURL": "https://github.com/google/blockly/wiki/Logic#values", "LOGIC_BOOLEAN_TRUE": "참", "LOGIC_BOOLEAN_FALSE": "거짓", "LOGIC_BOOLEAN_TOOLTIP": "참 혹은 거짓 모두 반환합니다.", + "LOGIC_NULL_HELPURL": "https://ko.wikipedia.org/wiki/널러블_타입", "LOGIC_NULL": "빈 값", "LOGIC_NULL_TOOLTIP": "빈 값을 반환합니다.", - "LOGIC_TERNARY_HELPURL": "https://ko.wikipedia.org/wiki/물음표", + "LOGIC_TERNARY_HELPURL": "https://ko.wikipedia.org/wiki/%3F:", "LOGIC_TERNARY_CONDITION": "테스트", "LOGIC_TERNARY_IF_TRUE": "만약 참이라면", "LOGIC_TERNARY_IF_FALSE": "만약 거짓이라면", @@ -199,13 +200,16 @@ "MATH_ONLIST_TOOLTIP_STD_DEV": "이 리스트의 표준 편차를 반환합니다.", "MATH_ONLIST_OPERATOR_RANDOM": "목록의 임의 항목", "MATH_ONLIST_TOOLTIP_RANDOM": "목록에서 임의의 아이템을 돌려줍니다.", + "MATH_MODULO_HELPURL": "https://ko.wikipedia.org/wiki/모듈로", "MATH_MODULO_TITLE": "%1 ÷ %2의 나머지", "MATH_MODULO_TOOLTIP": "첫 번째 수를 두 번째 수로 나눈, 나머지 값을 돌려줍니다.", "MATH_CONSTRAIN_HELPURL": "https://ko.wikipedia.org/wiki/클램핑_(그래픽)", "MATH_CONSTRAIN_TITLE": "%1의 값을, 최소 %2 최대 %3으로 조정", "MATH_CONSTRAIN_TOOLTIP": "어떤 수를, 특정 범위의 값이 되도록 강제로 조정합니다.", + "MATH_RANDOM_INT_HELPURL": "https://ko.wikipedia.org/wiki/난수 발생기", "MATH_RANDOM_INT_TITLE": "랜덤정수(%1<= n <=%2)", "MATH_RANDOM_INT_TOOLTIP": "두 주어진 제한된 범위 사이의 임의 정수값을 돌려줍니다.", + "MATH_RANDOM_FLOAT_HELPURL": "https://ko.wikipedia.org/wiki/난수 발생기", "MATH_RANDOM_FLOAT_TITLE_RANDOM": "임의 분수", "MATH_RANDOM_FLOAT_TOOLTIP": "0.0 (포함)과 1.0 (배타적) 사이의 임의 분수 값을 돌려줍니다.", "MATH_ATAN2_TITLE": "X:%1 Y:%2의 atan2", @@ -333,13 +337,14 @@ "LISTS_SPLIT_TOOLTIP_JOIN": "구분 기호로 구분하여 텍스트 목록을 하나의 텍스트에 병합합니다.", "LISTS_REVERSE_MESSAGE0": "%1 뒤집기", "LISTS_REVERSE_TOOLTIP": "리스트의 복사본을 뒤집습니다.", - "VARIABLES_GET_HELPURL": "https://ko.wikipedia.org/wiki/%EB%B3%80%EC%88%98_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)", + "VARIABLES_GET_HELPURL": "https://github.com/google/blockly/wiki/Variables#get", "VARIABLES_GET_TOOLTIP": "변수에 저장 되어있는 값을 돌려줍니다.", "VARIABLES_GET_CREATE_SET": "'집합 %1' 생성", - "VARIABLES_SET_HELPURL": "https://ko.wikipedia.org/wiki/%EB%B3%80%EC%88%98_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)", + "VARIABLES_SET_HELPURL": "https://github.com/google/blockly/wiki/Variables#set", "VARIABLES_SET": "%1를 %2로 설정", "VARIABLES_SET_TOOLTIP": "변수의 값을 입력한 값으로 변경해 줍니다.", "VARIABLES_SET_CREATE_GET": "'%1 값 읽기' 블럭 생성", + "PROCEDURES_DEFNORETURN_HELPURL": "https://ko.wikipedia.org/wiki/함수_(컴퓨터_과학)", "PROCEDURES_DEFNORETURN_TITLE": "함수", "PROCEDURES_DEFNORETURN_PROCEDURE": "함수 이름", "PROCEDURES_BEFORE_PARAMS": "사용:", diff --git a/msg/json/lb.json b/msg/json/lb.json index e373761e652..900ce30a23d 100644 --- a/msg/json/lb.json +++ b/msg/json/lb.json @@ -55,7 +55,7 @@ "CONTROLS_REPEAT_INPUT_DO": "maach", "CONTROLS_WHILEUNTIL_OPERATOR_WHILE": "Widderhuel soulaang", "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "widderhuele bis", - "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "Féiert d'Uweisungen aus, soulaang wéi de Wäert richteg ass", + "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "Féiert d'Uweisungen aus, soulaang wéi de Wäert wouer ass.", "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "Féiert d'Uweisungen aus, soulaang wéi de Wäert falsch ass.", "CONTROLS_FOR_TITLE": "zielt mat %1 vun %2 bis %3 mat %4", "CONTROLS_FOREACH_TITLE": "fir all Element %1 an der Lëscht %2", diff --git a/msg/json/nb.json b/msg/json/nb.json index 55a59c8a584..9b5d3773c57 100644 --- a/msg/json/nb.json +++ b/msg/json/nb.json @@ -306,6 +306,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "gjør noe", "PROCEDURES_BEFORE_PARAMS": "med:", "PROCEDURES_CALL_BEFORE_PARAMS": "med:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Kan ikke kjøre den brukerdefinerte funksjonen «%1» fordi definisjonsblokken er deaktivert.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Opprett en funksjon som ikke har noe resultat.", "PROCEDURES_DEFNORETURN_COMMENT": "Beskriv denne funksjonen…", "PROCEDURES_DEFRETURN_RETURN": "returner", diff --git a/msg/json/nl.json b/msg/json/nl.json index 914a8ef8ded..694a53ff6c7 100644 --- a/msg/json/nl.json +++ b/msg/json/nl.json @@ -44,7 +44,7 @@ "REDO": "Opnieuw", "CHANGE_VALUE_TITLE": "Waarde wijzigen:", "RENAME_VARIABLE": "Variabele hernoemen...", - "RENAME_VARIABLE_TITLE": "Alle variabelen \"%1\" hernoemen naar:", + "RENAME_VARIABLE_TITLE": "Alle variabelen “%1” hernoemen tot:", "NEW_VARIABLE": "Variabele maken...", "NEW_STRING_VARIABLE": "Tekstvariabele maken...", "NEW_NUMBER_VARIABLE": "Numeriek variabele maken...", @@ -330,6 +330,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "doe iets", "PROCEDURES_BEFORE_PARAMS": "met:", "PROCEDURES_CALL_BEFORE_PARAMS": "met:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Kan de door de gebruiker gedefinieerde functie ‘%1’ niet uitvoeren omdat het definitieblok is uitgeschakeld.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Maakt een functie zonder uitvoer.", "PROCEDURES_DEFNORETURN_COMMENT": "Deze functie beschrijven...", "PROCEDURES_DEFRETURN_HELPURL": "https://nl.wikipedia.org/wiki/Subprogramma", diff --git a/msg/json/pa.json b/msg/json/pa.json index e784ddc391b..e0ff05007ce 100644 --- a/msg/json/pa.json +++ b/msg/json/pa.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "AnupamM", + "Cabal", "Jimidar", "Tow", "ਪ੍ਰਚਾਰਕ" @@ -25,12 +26,13 @@ "DISABLE_BLOCK": "ਬਲਾਕ ਬੰਦ ਕਰੋ", "ENABLE_BLOCK": "ਬਲਾਕ ਚਾਲੂ ਕਰੋ", "HELP": "ਮਦਦ", - "UNDO": "ਅਣਕੀਤਾ ਕਰੋ", + "UNDO": "ਰੱਦ ਕਰੋ", "REDO": "ਮੁੜ ਕਰੋ", "CHANGE_VALUE_TITLE": "ਮੁੱਲ ਬਦਲੋ:", "COLOUR_PICKER_TOOLTIP": "ਰੰਗ-ਫੱਟੀ ਵਿੱਚੋਂ ਰੰਗ ਚੁਣੋ", "COLOUR_RANDOM_TITLE": "ਰਲ਼ਵਾਂ ਰੰਗ", "COLOUR_RANDOM_TOOLTIP": "ਰਲ਼ਵਾਂ ਰੰਗ ਚੁਣੋ", + "COLOUR_RGB_TITLE": "ਦੇ ਨਾਲ ਰੰਗ ਕਰੋ", "COLOUR_RGB_RED": "ਲਾਲ", "COLOUR_RGB_GREEN": "ਹਰਾ", "COLOUR_RGB_BLUE": "ਨੀਲਾ", @@ -39,10 +41,13 @@ "COLOUR_BLEND_COLOUR1": "ਰੰਗ 1", "COLOUR_BLEND_COLOUR2": "ਰੰਗ 2", "COLOUR_BLEND_RATIO": "ਅਨੁਪਾਤ", - "COLOUR_BLEND_TOOLTIP": "ਦਿੱਤੇ ਅਨੁਪਾਤ (0.0 - 1.0) ਅਨੁਸਾਰ ਦੋ ਰੰਗ ਮਿਲਾਓ।", + "COLOUR_BLEND_TOOLTIP": "ਦਿੱਤੇ ਅਨੁਪਾਤ (0.0 - 1.0) ਮੁਤਾਬਕ ਦੋ ਰੰਗ ਮਿਲਾਓ।", "CONTROLS_REPEAT_TITLE": "%1 ਵਾਰੀ ਦੁਹਰਾਉ", "CONTROLS_REPEAT_INPUT_DO": "ਕਰੋ", + "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "ਦੁਹਰਾਓ ਜਦੋਂ ਤੱਕ", + "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "ਘੇਰੇ ਚੋਂ ਬਾਹਰ ਕੱਢੋ", "CONTROLS_IF_MSG_IF": "ਜੇ", + "CONTROLS_IF_MSG_ELSEIF": "ਹੋਰ ਜੇ", "CONTROLS_IF_MSG_ELSE": "ਹੋਰ", "LOGIC_OPERATION_AND": "ਅਤੇ", "LOGIC_OPERATION_OR": "ਜਾਂ", @@ -50,7 +55,7 @@ "LOGIC_BOOLEAN_FALSE": "ਝੂਠ", "LOGIC_NULL": "ਨੱਲ", "LOGIC_NULL_TOOLTIP": "ਨੱਲ ਮੋੜਦਾ ਹੈ।", - "LOGIC_TERNARY_CONDITION": "ਟੈਸਟ", + "LOGIC_TERNARY_CONDITION": "ਪ੍ਰੀਖਿਆ", "LOGIC_TERNARY_IF_TRUE": "ਜੇ ਸੱਚ", "LOGIC_TERNARY_IF_FALSE": "ਜੇ ਝੂਠ", "MATH_NUMBER_TOOLTIP": "ਇੱਕ ਅੰਕ", @@ -60,9 +65,10 @@ "MATH_SINGLE_OP_ROOT": "ਵਰਗ ਮੂਲ", "MATH_SINGLE_TOOLTIP_ROOT": "ਇੱਕ ਅੰਕ ਦਾ ਵਰਗ ਮੂਲ ਮੋੜੋ।", "LISTS_GET_INDEX_FIRST": "ਪਹਿਲਾ", - "LISTS_GET_INDEX_LAST": "ਆਖ਼ਰੀ", + "LISTS_GET_INDEX_LAST": "ਆਖੀਰਲਾ", "LISTS_GET_INDEX_RANDOM": "ਰਲ਼ਵਾਂ", - "LISTS_SORT_ORDER_DESCENDING": "ਘਟਦੇ ਕ੍ਰਮ ਵਿੱਚ", + "LISTS_SORT_ORDER_ASCENDING": "ਚੜ੍ਹਦੀ ਤਰਤੀਬ ਵਿੱਚ", + "LISTS_SORT_ORDER_DESCENDING": "ਲਹਿੰਦੀ ਤਰਤੀਬ ਵਿੱਚ", "PROCEDURES_DEFRETURN_RETURN": "ਮੋੜੋ", "DIALOG_OK": "ਠੀਕ ਹੈ।", "DIALOG_CANCEL": "ਰੱਦ ਕਰੋ" diff --git a/msg/json/pl.json b/msg/json/pl.json index c94d4875329..4d98e1e2cb2 100644 --- a/msg/json/pl.json +++ b/msg/json/pl.json @@ -331,6 +331,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "zrób coś", "PROCEDURES_BEFORE_PARAMS": "z:", "PROCEDURES_CALL_BEFORE_PARAMS": "z:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Nie można uruchomić funkcji zdefiniowanej przez użytkownika '%1', ponieważ blok definicji jest wyłączony.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Tworzy funkcję nie posiadającą wyjścia.", "PROCEDURES_DEFNORETURN_COMMENT": "Opisz tę funkcję...", "PROCEDURES_DEFRETURN_RETURN": "zwróć", diff --git a/msg/json/pms.json b/msg/json/pms.json index 0f54aa66533..99e92274cdc 100644 --- a/msg/json/pms.json +++ b/msg/json/pms.json @@ -300,6 +300,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "fé cheicòs", "PROCEDURES_BEFORE_PARAMS": "con:", "PROCEDURES_CALL_BEFORE_PARAMS": "con:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Impossìbil fé marcé la fonsion definìa da l'utent '%1' përché ël blòch ëd definission a l'é disativà.", "PROCEDURES_DEFNORETURN_TOOLTIP": "A crea na fonsion sensa surtìa.", "PROCEDURES_DEFNORETURN_COMMENT": "Descrive sa fonsion...", "PROCEDURES_DEFRETURN_RETURN": "artorn", diff --git a/msg/json/pt-br.json b/msg/json/pt-br.json index 2754c603937..01fa44697d4 100644 --- a/msg/json/pt-br.json +++ b/msg/json/pt-br.json @@ -17,6 +17,7 @@ "Lc97", "Lowvy", "Luk3", + "Maakhai", "Mauricio", "McDutchie", "Mordecaista", @@ -133,7 +134,7 @@ "LOGIC_TERNARY_TOOLTIP": "Avalia a condição em \"teste\". Se a condição for verdadeira retorna o valor \"se verdadeiro\", senão retorna o valor \"se falso\".", "MATH_NUMBER_HELPURL": "https://pt.wikipedia.org/wiki/N%C3%BAmero", "MATH_NUMBER_TOOLTIP": "Um número.", - "MATH_TRIG_SIN": "sin", + "MATH_TRIG_SIN": "sen", "MATH_TRIG_COS": "cos", "MATH_TRIG_TAN": "tan", "MATH_TRIG_ASIN": "asin", @@ -342,6 +343,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "faça algo", "PROCEDURES_BEFORE_PARAMS": "com:", "PROCEDURES_CALL_BEFORE_PARAMS": "com:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Não foi possível executar a função %1 definida pelo usuário porque o bloco de definição está desabilitado.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Cria uma função que não tem retorno.", "PROCEDURES_DEFNORETURN_COMMENT": "Descreva esta função...", "PROCEDURES_DEFRETURN_HELPURL": "https://pt.wikipedia.org/wiki/M%C3%A9todo_(programa%C3%A7%C3%A3o)", diff --git a/msg/json/pt.json b/msg/json/pt.json index a024b1b30f1..f51544acc9b 100644 --- a/msg/json/pt.json +++ b/msg/json/pt.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "Athena in Wonderland", + "B3rnas", "Diniscoelho", "Fúlvio", "Hamilton Abreu", @@ -332,6 +333,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "faz algo", "PROCEDURES_BEFORE_PARAMS": "com:", "PROCEDURES_CALL_BEFORE_PARAMS": "com:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Não é possível executar a função definida pelo usuário '%1' porque o bloco de definição está desabilitado.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Cria uma função que não tem retorno.", "PROCEDURES_DEFNORETURN_COMMENT": "Descreva esta função...", "PROCEDURES_DEFRETURN_RETURN": "retorna", diff --git a/msg/json/qqq.json b/msg/json/qqq.json index fcd8897bd04..e0310717daf 100644 --- a/msg/json/qqq.json +++ b/msg/json/qqq.json @@ -5,6 +5,7 @@ "Amire80", "Espertus", "Liuxinyu970226", + "McDutchie", "Metalhead64", "Nike", "Robby", @@ -18,8 +19,8 @@ "ADD_COMMENT": "context menu - Add a descriptive comment to the selected block.", "REMOVE_COMMENT": "context menu - Remove the descriptive comment from the selected block.", "DUPLICATE_COMMENT": "context menu - Make a copy of the selected workspace comment.\n{{Identical|Duplicate}}", - "EXTERNAL_INPUTS": "context menu - Change from 'external' to 'inline' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].", - "INLINE_INPUTS": "context menu - Change from 'internal' to 'external' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].", + "EXTERNAL_INPUTS": "context menu - Change from 'external' to 'inline' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].\n\nThe opposite of {{msg-blockly|INLINE INPUTS}}.", + "INLINE_INPUTS": "context menu - Change from 'internal' to 'external' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].\n\nThe opposite of {{msg-blockly|EXTERNAL INPUTS}}.", "DELETE_BLOCK": "context menu - Permanently delete the selected block.", "DELETE_X_BLOCKS": "context menu - Permanently delete the %1 selected blocks.\n\nParameters:\n* %1 - an integer greater than 1.", "DELETE_ALL_BLOCKS": "confirmation prompt - Question the user if they really wanted to permanently delete all %1 blocks.\n\nParameters:\n* %1 - an integer greater than 1.", diff --git a/msg/json/sd.json b/msg/json/sd.json index f6e7b5f030d..a481d744e60 100644 --- a/msg/json/sd.json +++ b/msg/json/sd.json @@ -19,7 +19,7 @@ "EXTERNAL_INPUTS": "ٻاهريون داخلائون", "INLINE_INPUTS": "اِنلائين اِن پٽس", "DELETE_BLOCK": "بلاڪ ڊاهيو", - "DELETE_X_BLOCKS": "1٪ بلاڪ ڊاهيو", + "DELETE_X_BLOCKS": "%1 بلاڪ ڊاهيو", "DELETE_ALL_BLOCKS": "سڀ %1 بلاڪ ڊاھيون؟", "CLEAN_UP": "بلاڪ صاف ڪيو", "COLLAPSE_BLOCK": "بلاڪ ڍڪيو", diff --git a/msg/json/sk.json b/msg/json/sk.json index ab400afd573..f458f0f8e4d 100644 --- a/msg/json/sk.json +++ b/msg/json/sk.json @@ -9,6 +9,7 @@ "Marian.stano", "Mark", "Nykta 1917", + "Oujon", "Pmikolas44", "TomášPolonec", "Yardom78" @@ -47,6 +48,7 @@ "NEW_VARIABLE_TITLE": "Názov novej premennej:", "VARIABLE_ALREADY_EXISTS": "Premenná s názvom %1 už existuje.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Premenná s názvom '%1' už existuje pre inú premennú typu '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Premenná s názvom '%1' už existuje ako parameter v procedúre '%2'.", "DELETE_VARIABLE_CONFIRMATION": "Odstrániť %1 použití premennej '%2'?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Nie je možné zmazať premennú „%1“, pretože je súčasťou definície funkcie „%2“", "DELETE_VARIABLE": "Odstrániť premennú '%1'", @@ -111,6 +113,9 @@ "LOGIC_TERNARY_IF_FALSE": "ak nepravda", "LOGIC_TERNARY_TOOLTIP": "Skontroluj podmienku testom. Ak je podmienka pravda, vráť hodnotu \"ak pravda\", inak vráť hodnotu \"ak nepravda\".", "MATH_NUMBER_TOOLTIP": "Číslo.", + "MATH_TRIG_SIN": "Sin", + "MATH_TRIG_COS": "Cos", + "MATH_TRIG_TAN": "Tan", "MATH_TRIG_ASIN": "arcsin", "MATH_TRIG_ACOS": "arccos", "MATH_TRIG_ATAN": "arctan", @@ -247,6 +252,7 @@ "LISTS_GET_INDEX_GET": "zisti", "LISTS_GET_INDEX_GET_REMOVE": "zisti a odstráň", "LISTS_GET_INDEX_REMOVE": "odstráň", + "LISTS_GET_INDEX_FROM_START": "#", "LISTS_GET_INDEX_FROM_END": "# od konca", "LISTS_GET_INDEX_FIRST": "prvý", "LISTS_GET_INDEX_LAST": "posledný", @@ -306,6 +312,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "urob niečo", "PROCEDURES_BEFORE_PARAMS": "s:", "PROCEDURES_CALL_BEFORE_PARAMS": "s:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Nie je možné spustiť užívateľom definovanú funkciu '%1', pretože blok definície je zakázaný.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Vytvorí funciu bez výstupu.", "PROCEDURES_DEFNORETURN_COMMENT": "Doplň, čo robí táto funkcia...", "PROCEDURES_DEFRETURN_RETURN": "vrátiť", diff --git a/msg/json/sr.json b/msg/json/sr.json index 4741be7485a..64ffd6559ee 100644 --- a/msg/json/sr.json +++ b/msg/json/sr.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Aca", "Acamicamacaraca", "BadDog", "Kizule", diff --git a/msg/json/sv.json b/msg/json/sv.json index 45c3864d6a8..eebfb269b86 100644 --- a/msg/json/sv.json +++ b/msg/json/sv.json @@ -189,7 +189,7 @@ "MATH_RANDOM_FLOAT_TOOLTIP": "Ger tillbaka ett slumpat decimaltal mellan 0.0 (inkluderat) och 1.0 (exkluderat).", "MATH_ATAN2_TITLE": "atan2 av X:%1 Y:%2", "MATH_ATAN2_TOOLTIP": "Returnerar arcustangens av punkt (X, Y) i grader från -180 till 180.", - "TEXT_TEXT_HELPURL": "https://sv.wikipedia.org/wiki/Str%C3%A4ng_%28data%29", + "TEXT_TEXT_HELPURL": "https://sv.wikipedia.org/wiki/Str%C3%A4ng_(data)", "TEXT_TEXT_TOOLTIP": "En bokstav, ord eller textrad.", "TEXT_JOIN_TITLE_CREATEWITH": "skapa text med", "TEXT_JOIN_TOOLTIP": "Skapa en textbit genom att sammanfoga ett valfritt antal föremål.", @@ -322,6 +322,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "göra något", "PROCEDURES_BEFORE_PARAMS": "med:", "PROCEDURES_CALL_BEFORE_PARAMS": "med:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Kan inte att köra den användardefinierade funktionen \"%1\" eftersom definitionsblocket är inaktiverat.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Skapar en funktion utan output.", "PROCEDURES_DEFNORETURN_COMMENT": "Beskriv denna funktion...", "PROCEDURES_DEFRETURN_HELPURL": "https://sv.wikipedia.org/wiki/Funktion_(programmering)", @@ -329,7 +330,9 @@ "PROCEDURES_DEFRETURN_TOOLTIP": "Skapar en funktion med output.", "PROCEDURES_ALLOW_STATEMENTS": "tillåta uttalanden", "PROCEDURES_DEF_DUPLICATE_WARNING": "Varning: Denna funktion har dubbla parametrar.", + "PROCEDURES_CALLNORETURN_HELPURL": "https://sv.wikipedia.org/wiki/Funktion_(programmering)", "PROCEDURES_CALLNORETURN_TOOLTIP": "Kör den användardefinierade funktionen \"%1\".", + "PROCEDURES_CALLRETURN_HELPURL": "https://sv.wikipedia.org/wiki/Funktion_(programmering)", "PROCEDURES_CALLRETURN_TOOLTIP": "Kör den användardefinierade funktionen \"%1\" och använd resultatet av den.", "PROCEDURES_MUTATORCONTAINER_TITLE": "inmatningar", "PROCEDURES_MUTATORCONTAINER_TOOLTIP": "Lägg till, ta bort och ändra ordningen för inmatningar till denna funktion.", diff --git a/msg/json/synonyms.json b/msg/json/synonyms.json index 9fc089ebea7..1af8184700d 100644 --- a/msg/json/synonyms.json +++ b/msg/json/synonyms.json @@ -1,5 +1,4 @@ { - "#": "Automatically generated, do not edit this file!", "CONTROLS_FOREACH_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", "CONTROLS_FOR_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", "CONTROLS_IF_ELSEIF_TITLE_ELSEIF": "CONTROLS_IF_MSG_ELSEIF", @@ -8,6 +7,7 @@ "CONTROLS_IF_MSG_THEN": "CONTROLS_REPEAT_INPUT_DO", "CONTROLS_WHILEUNTIL_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", "LISTS_CREATE_WITH_ITEM_TITLE": "VARIABLES_DEFAULT_NAME", + "LISTS_GET_INDEX_HELPURL": "LISTS_INDEX_OF_HELPURL", "LISTS_GET_INDEX_INPUT_IN_LIST": "LISTS_INLIST", "LISTS_GET_SUBLIST_INPUT_IN_LIST": "LISTS_INLIST", "LISTS_INDEX_OF_INPUT_IN_LIST": "LISTS_INLIST", @@ -19,4 +19,4 @@ "PROCEDURES_DEFRETURN_TITLE": "PROCEDURES_DEFNORETURN_TITLE", "TEXT_APPEND_VARIABLE": "VARIABLES_DEFAULT_NAME", "TEXT_CREATE_JOIN_ITEM_TITLE_ITEM": "VARIABLES_DEFAULT_NAME" -} \ No newline at end of file +} diff --git a/msg/json/tcy.json b/msg/json/tcy.json index 06704e50b7b..712f9dfc9ba 100644 --- a/msg/json/tcy.json +++ b/msg/json/tcy.json @@ -28,7 +28,7 @@ "EXPAND_ALL": "ಮಾತಾ ತಡೆಕ್ಲೆನ ಮಾಹಿತಿನ್ ಪರಡಾವು", "DISABLE_BLOCK": "ಬ್ಲಾಕ್‍ನ್ ದೆತ್ತ್‌ಪಾಡ್", "ENABLE_BLOCK": "ತಡೆನ್ ಸಕ್ರಿಯೊ ಮಲ್ಪು", - "HELP": "ಸಹಾಯೊ", + "HELP": "ಸಕಾಯೊ", "UNDO": "ದುಂಬುದಲೆಕೊ", "REDO": "ಕುಡ ಮಲ್ಪು", "CHANGE_VALUE_TITLE": "ಮೌಲ್ಯೊನು ಬದಲ್ ಮಲ್ಪು", @@ -311,6 +311,6 @@ "PROCEDURES_IFRETURN_TOOLTIP": "ಮೌಲ್ಯೊ ಸತ್ಯೊ ಆಂಡ, ರಡ್ಡನೆ ಮೌಲ್ಯೊನು ಪಿರಕೊರು.", "PROCEDURES_IFRETURN_WARNING": "ಎಚ್ಚರಿಕೆ: ಒಂಜಿ ಕಾರ್ಯ ವ್ಯಾಕ್ಯಾನೊದುಲಯಿ ಮಾತ್ರ ಈ ತಡೆನ್ ಗಲಸೊಲಿ.", "WORKSPACE_COMMENT_DEFAULT_TEXT": "ದಾದಾಂಡಲ ಪನ್ಲೇ...", - "DIALOG_OK": "ಅವು", + "DIALOG_OK": "ಆವು", "DIALOG_CANCEL": "ಉಂತಾಲೆ" } diff --git a/msg/json/tdd.json b/msg/json/tdd.json index 2bb756d2367..2f63c5787de 100644 --- a/msg/json/tdd.json +++ b/msg/json/tdd.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "AeyTaiNuea", + "Dai Meng Mao Long", "咽頭べさ" ] }, diff --git a/msg/json/tl.json b/msg/json/tl.json index fad9ab9f91c..1eb04100699 100644 --- a/msg/json/tl.json +++ b/msg/json/tl.json @@ -1,23 +1,43 @@ { "@metadata": { "authors": [ + "GinawaSaHapon", "아라" ] }, - "DUPLICATE_BLOCK": "Kaparehas", - "ADD_COMMENT": "Dagdag komento", - "EXTERNAL_INPUTS": "Panlabas na Inputs", - "INLINE_INPUTS": "Inline na Inputs", - "DELETE_BLOCK": "burahin ang bloke", - "DELETE_X_BLOCKS": "burahin %1 ng bloke", - "COLLAPSE_BLOCK": "bloke", - "COLLAPSE_ALL": "bloke", - "EXPAND_BLOCK": "Palawakin ang Block", - "EXPAND_ALL": "Palawakin ang Blocks", - "DISABLE_BLOCK": "Ipangwalang bisa ang Block", - "ENABLE_BLOCK": "Bigyan ng bisa ang Block", + "VARIABLES_DEFAULT_NAME": "item", + "UNNAMED_KEY": "walang pangalan", + "TODAY": "Ngayon", + "DUPLICATE_BLOCK": "I-duplicate", + "ADD_COMMENT": "Magkomento", + "REMOVE_COMMENT": "Alisin ang Komento", + "DUPLICATE_COMMENT": "I-duplicate ang Komento", + "EXTERNAL_INPUTS": "Input sa Labas", + "INLINE_INPUTS": "Input sa Linya", + "DELETE_BLOCK": "Alisin ang Block", + "DELETE_X_BLOCKS": "Alisin ang %1 (na) Block", + "DELETE_ALL_BLOCKS": "Alisin ang %1 (na) block?", + "CLEAN_UP": "Ayusin ang mga Block", + "COLLAPSE_BLOCK": "Itago ang Block", + "COLLAPSE_ALL": "Itago ang mga Block", + "EXPAND_BLOCK": "Buksan ang Block", + "EXPAND_ALL": "Buksan ang mga Block", + "DISABLE_BLOCK": "I-disable ang Block", + "ENABLE_BLOCK": "I-enable ang Block", "HELP": "Tulong", - "CHANGE_VALUE_TITLE": "pagbago ng value:", + "UNDO": "I-undo", + "REDO": "I-redo", + "CHANGE_VALUE_TITLE": "Baguhin ang value:", + "RENAME_VARIABLE": "I-rename ang variable...", + "RENAME_VARIABLE_TITLE": "I-rename ang lahat ng mga '%1' na variable bilang:", + "NEW_VARIABLE": "Gumawa ng variable...", + "NEW_STRING_VARIABLE": "Gumawa ng string variable...", + "NEW_NUMBER_VARIABLE": "Gumawa ng number variable...", + "NEW_COLOUR_VARIABLE": "Gumawa ng color variable...", + "NEW_VARIABLE_TYPE_TITLE": "Uri ng variable:", + "NEW_VARIABLE_TITLE": "Pangalan ng variable:", + "VARIABLE_ALREADY_EXISTS": "Meron na'ng variable na '%1'.", + "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Meron na'ng variable na '%1' sa ibang uri: '%2'", "COLOUR_PICKER_TOOLTIP": "pagpili ng kulay sa paleta.", "COLOUR_RANDOM_TITLE": "iba ibang kulay", "COLOUR_RANDOM_TOOLTIP": "pagpili ng iba't ibang kulay.", diff --git a/msg/json/tr.json b/msg/json/tr.json index b65a5ce31f7..bec55f10263 100644 --- a/msg/json/tr.json +++ b/msg/json/tr.json @@ -342,6 +342,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "bir şey yap", "PROCEDURES_BEFORE_PARAMS": "ile:", "PROCEDURES_CALL_BEFORE_PARAMS": "ile:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Kullanıcı tanımlı '%1' işlevi, tanımlama bloğu devre dışı olduğundan çalıştırılamıyor.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Çıkışı olmayan bir işlev oluşturur.", "PROCEDURES_DEFNORETURN_COMMENT": "Bu işlevi açıklayın...", "PROCEDURES_DEFRETURN_HELPURL": "https://tr.wikipedia.org/wiki/Altyordam", diff --git a/msg/json/ug-arab.json b/msg/json/ug-arab.json index 37dd257a3f7..07af758f3ba 100644 --- a/msg/json/ug-arab.json +++ b/msg/json/ug-arab.json @@ -2,15 +2,18 @@ "@metadata": { "authors": [ "HushBeg", + "Nail123Real", "Uzdil", "چۈشكۈن" ] }, "VARIABLES_DEFAULT_NAME": "تۈر", + "UNNAMED_KEY": "نامسىز", "TODAY": "بۈگۈن", "DUPLICATE_BLOCK": "كۆچۈرۈش", "ADD_COMMENT": "ئىزاھات قوشۇش", "REMOVE_COMMENT": "ئىزاھاتنى ئۆچۈرۈش", + "DUPLICATE_COMMENT": "تەكرار باھا", "EXTERNAL_INPUTS": "سىرتقى كىرگۈزۈش", "INLINE_INPUTS": "تاق قۇرلۇق كىرگۈزۈش", "DELETE_BLOCK": "بۆلەك ئۆچۈرۈش", @@ -30,10 +33,16 @@ "RENAME_VARIABLE": "ئۆزگەرگۈچى مىقدارغا قايتا نام قويۇش", "RENAME_VARIABLE_TITLE": "بارلىق بۆلەك “%1\" ئۆزگەرگۈچى مىقدار قايتا ناملىنىپ :", "NEW_VARIABLE": "ئۆزگەرگۈچى مىقدار ... قۇرۇش", + "NEW_STRING_VARIABLE": "قۇر ئۆزگەرگۈچى مىقدار قۇر...", + "NEW_NUMBER_VARIABLE": "سان ئۆزگەرگۈچى مىقدار قۇر...", + "NEW_COLOUR_VARIABLE": "سان ئۆزگەرگۈچى مىقدار قۇر...", + "NEW_VARIABLE_TYPE_TITLE": "يېڭى ئۆزگەرگۈچى تىپى:", "NEW_VARIABLE_TITLE": "يېڭى ئۆزگەرگۈچى مىقدار نامى:", "VARIABLE_ALREADY_EXISTS": "ئىسم مەۋجۇت “%1” ئۆزگەرگۈچى", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "ئىسىملىك“%1” ئۆزگەرگۈچى مىقدار مەۋجۇت بولۇپ تۇرىدۇ ، لېكىن يەنە بىر ئۆزگەرگۈچى مىقدار تىپى بولۇش سۈپىتى بىلەن “%2” مەۋجۇت .", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "'%1' دېگەن ئۆزگەرگۈچى مىقدار '%2' تەرتىپىدە پارامېتىر سۈپىتىدە مەۋجۇت.", "DELETE_VARIABLE_CONFIRMATION": "ئۆچۈرۈش “%2” ئۆزگەرگۈچى مىقدار%1 ئىشلىتىلىش ئورنى بارمۇ؟", + "CANNOT_DELETE_VARIABLE_PROCEDURE": "ئۆزگەرگۈچى مىقدار '%1' نى ئۆچۈرەلمەيدۇ ، چۈنكى ئۇ '%2' فۇنكىسىيەسىنىڭ ئېنىقلىمىسىنىڭ بىر قىسمى.", "DELETE_VARIABLE": "“%1” ئۆزگەرگۈچى مىقدارنى ئۆچۈرۈش", "COLOUR_PICKER_HELPURL": "https://zh.wikipedia.org/wiki/رەڭگى", "COLOUR_PICKER_TOOLTIP": "تاختىدىن رەڭنى تاللاڭ", @@ -43,15 +52,24 @@ "COLOUR_RGB_RED": "قىزىل", "COLOUR_RGB_GREEN": "يېشىل", "COLOUR_RGB_BLUE": "كۆك", + "COLOUR_RGB_TOOLTIP": "بەلگىلەنگەن مىقداردا قىزىل ، يېشىل ۋە كۆك رەڭ ھاسىل قىلىڭ. بارلىق قىممەتلەر چوقۇم 0 دىن 100 گىچە بولۇشى كېرەك.", "COLOUR_BLEND_TITLE": "ئارىلاش", "COLOUR_BLEND_COLOUR1": "رەڭ 1", "COLOUR_BLEND_COLOUR2": "رەڭ 2", "COLOUR_BLEND_RATIO": "نىسبەت", + "COLOUR_BLEND_TOOLTIP": "ئىككى خىل رەڭنى مەلۇم نىسبەت بىلەن ئارىلاشتۇرۇڭ (0.0 - 1.0).", "CONTROLS_REPEAT_HELPURL": "https://zh.wikipedia.org/wiki/Forئايلىنىش", "CONTROLS_REPEAT_TITLE": "تەكرار %1قېتىم", "CONTROLS_REPEAT_INPUT_DO": "ئىجرا", + "CONTROLS_REPEAT_TOOLTIP": "بەزى بايانلارنى بىر نەچچە قېتىم قىلىڭ.", "CONTROLS_WHILEUNTIL_OPERATOR_WHILE": "تەكرار بولۇش", "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "تەكرارلىقى", + "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "بىر قىممەت راست بولسىمۇ ، بەزى بايانلارنى قىلىڭ.", + "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "قىممەت يالغان بولسىمۇ ، بەزى بايانلارنى قىلىڭ.", + "CONTROLS_FOR_TOOLTIP": "ئۆزگەرگۈچى مىقدار '%1' باشلىنىش نومۇرىدىن ئاخىرقى سانغىچە بولغان قىممەتنى ئېلىپ ، بەلگىلەنگەن ئارىلىقنى ھېسابلاپ ، كۆرسىتىلگەن بۆلەكلەرنى قىلىڭ.", + "CONTROLS_FOR_TITLE": "%1 بىلەن%2 دىن%3 گىچە ھېسابلاڭ %4", + "CONTROLS_FOREACH_TITLE": "ھەر بىر تۈر ئۈچۈن%1 تىزىملىكتىكى%2", + "CONTROLS_FOREACH_TOOLTIP": "تىزىملىكتىكى ھەر بىر تۈرگە ئۆزگەرگۈچى مىقدارنى «%1» قىلىپ تەڭشەڭ ، ئاندىن بەزى بايانلارنى قىلىڭ.", "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "ئۈزۈلۈپ ئايلىنىش", "CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE": "كىيىنكى قېتىم داۋاملىق ئايلىنىشن", "CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK": "ئۇنىڭ دەۋرىي ئۈزۈلۈش ئۆز ئىچىگە ئالىدۇ .", @@ -74,9 +92,12 @@ "LOGIC_COMPARE_TOOLTIP_LTE": "ئەگەر تۇنجى كىرگۈزۈش نەتىجىسى ئىككىنچى كىرگۈزۈش نەتىجىسى تىن تۆۋەن ياكى شۇنىڭغا تەڭ بولسا راستىنلا كەينىگە قايتسا .", "LOGIC_COMPARE_TOOLTIP_GT": "ئەگەر تۇنجى كىرگۈزۈش نەتىجىسى ئىشككىنچى چوڭ بولسا راستىنلا كەينىگە قايتسا .", "LOGIC_COMPARE_TOOLTIP_GTE": "ئەگەر تۇنجى كىرگۈزۈش نەتىجىدە ئىشككىنچى كىچىك بولسا راستىنلا كەينىگە قايتسا .", + "LOGIC_OPERATION_TOOLTIP_AND": "ئەگەر ھەر ئىككى كىرگۈزۈش توغرا بولسا ، ھەقىقىي قايتىڭ.", "LOGIC_OPERATION_AND": "ۋە", + "LOGIC_OPERATION_TOOLTIP_OR": "كەم دېگەندە كىرگۈزۈشنىڭ بىرى راست بولسا ھەقىقىي قايتىڭ.", "LOGIC_OPERATION_OR": "ياكى", "LOGIC_NEGATE_TITLE": "ئەمەس%1", + "LOGIC_NEGATE_TOOLTIP": "ئەگەر كىرگۈزۈش يالغان بولسا توغرا قايتىدۇ. ئەگەر كىرگۈزۈش راست بولسا يالغاننى قايتۇرىدۇ.", "LOGIC_BOOLEAN_TRUE": "ھەقىقىي", "LOGIC_BOOLEAN_FALSE": "يالغان", "LOGIC_BOOLEAN_TOOLTIP": "راست ياكى يالغان قايتىش", @@ -85,12 +106,31 @@ "LOGIC_TERNARY_CONDITION": "سىناق", "LOGIC_TERNARY_IF_TRUE": "ئەگەر راست بولسا", "LOGIC_TERNARY_IF_FALSE": "ئەگەر يالغان بولسا", + "LOGIC_TERNARY_TOOLTIP": "«سىناق» دىكى ئەھۋالنى تەكشۈرۈڭ. ئەگەر شەرت راست بولسا ، 'if true' قىممىتىنى قايتۇرىدۇ. بولمىسا 'if false' قىممىتىنى قايتۇرىدۇ.", "MATH_NUMBER_HELPURL": "https://zh.wikipedia.org/wiki/سان", "MATH_NUMBER_TOOLTIP": "بىر سان.", + "MATH_TRIG_SIN": "گۇناھ", + "MATH_TRIG_COS": "cos", + "MATH_TRIG_TAN": "tan", + "MATH_TRIG_ASIN": "گۇناھ", + "MATH_TRIG_ACOS": "acos", + "MATH_TRIG_ATAN": "atan", "MATH_ARITHMETIC_HELPURL": "https://zh.wikipedia.org/wiki/ئارىفمېتىكىلىق", + "MATH_ARITHMETIC_TOOLTIP_ADD": "ئىككى ساننىڭ يىغىندىسىنى قايتۇرۇڭ.", + "MATH_ARITHMETIC_TOOLTIP_MINUS": "ئىككى ساننىڭ پەرقىنى قايتۇرۇڭ.", + "MATH_ARITHMETIC_TOOLTIP_MULTIPLY": "ئىككى ساننىڭ مەھسۇلاتىنى قايتۇرۇڭ.", + "MATH_ARITHMETIC_TOOLTIP_DIVIDE": "ئىككى ساننىڭ سانىنى قايتۇرۇڭ.", + "MATH_ARITHMETIC_TOOLTIP_POWER": "بىرىنچى ساننى ئىككىنچى ساننىڭ كۈچىگە قايتۇرۇڭ.", "MATH_SINGLE_HELPURL": "https://zh.wikipedia.org/wiki/كۋادرات يىلتىز", "MATH_SINGLE_OP_ROOT": "كۋادرات يىلتىز", + "MATH_SINGLE_TOOLTIP_ROOT": "ساننىڭ كۋادرات يىلتىزىنى قايتۇرۇڭ.", "MATH_SINGLE_OP_ABSOLUTE": "مۇتلەق", + "MATH_SINGLE_TOOLTIP_ABS": "بىر ساننىڭ مۇتلەق قىممىتىنى قايتۇرۇڭ.", + "MATH_SINGLE_TOOLTIP_NEG": "بىر ساننىڭ رەت قىلىنىشىنى قايتۇرۇڭ.", + "MATH_SINGLE_TOOLTIP_LN": "بىر ساننىڭ تەبىئىي لوگارىزىمنى قايتۇرۇڭ.", + "MATH_SINGLE_TOOLTIP_LOG10": "بىر ساننىڭ ئاساسى 10 لوگارىزىمنى قايتۇرۇڭ.", + "MATH_SINGLE_TOOLTIP_EXP": "e نى ساننىڭ كۈچىگە قايتۇرۇڭ.", + "MATH_SINGLE_TOOLTIP_POW10": "10 نى ساننىڭ كۈچىگە قايتۇرۇڭ.", "MATH_TRIG_HELPURL": "https://zh.wikipedia.org/wiki/ترىگونومېتىرىيىلىك فۇنكسىيە", "MATH_CONSTANT_HELPURL": "https://zh.wikipedia.org/wiki/ماتېماتىكا تۇراقلىق سانى", "MATH_IS_EVEN": "جۈپ سان", @@ -106,6 +146,7 @@ "MATH_ROUND_OPERATOR_ROUND": "تۆۋەنگە تارتىڭ", "MATH_ROUND_OPERATOR_ROUNDUP": "تۆۋەنگە تارتىڭ", "MATH_ROUND_OPERATOR_ROUNDDOWN": "تۆۋەنگە تارتىڭ", + "MATH_ONLIST_OPERATOR_SUM": "تىزىملىكنىڭ يىغىندىسى", "MATH_ONLIST_OPERATOR_MIN": "جەدۋەل ئىچىدىكى ئەڭ كىچىك قىممەت", "MATH_ONLIST_TOOLTIP_MIN": "جەدۋەلدىكى ئەڭ كىچىك سانغا قايتىش", "MATH_ONLIST_OPERATOR_MAX": "جەدۋەلدىكى ئەڭ چوڭ قىممەت", @@ -115,18 +156,25 @@ "MATH_ONLIST_OPERATOR_MODE": "جەدۋەل ھالىتى", "MATH_MODULO_HELPURL": "https://zh.wikipedia.org/wiki/مودېل ھېسابى", "TEXT_CREATE_JOIN_TITLE_JOIN": "قوشۇش", + "TEXT_PRINT_TITLE": "بېسىپ چىقىرىش %1", + "TEXT_PRINT_TOOLTIP": "كۆرسىتىلگەن تېكىست ، سان ياكى باشقا قىممەتنى بېسىڭ.", + "LISTS_INLIST": "تىزىملىكتە", "LISTS_GET_INDEX_GET": "قولغا كەلتۈرۈش", "LISTS_GET_INDEX_REMOVE": "چىقىرىۋىتىش", + "LISTS_GET_INDEX_FROM_START": "#", + "LISTS_GET_INDEX_FROM_END": "# ئاخىرىدىن", "LISTS_GET_INDEX_FIRST": "تۇنجى", "LISTS_GET_INDEX_LAST": "ئاخىرقى", "LISTS_GET_INDEX_RANDOM": "خالىغانچە", "LISTS_SET_INDEX_SET": "تەڭشەك", "LISTS_SET_INDEX_INSERT": "قىستۇرۇڭ", + "LISTS_SET_INDEX_INPUT_TO": "دېگەندەك", "LISTS_SORT_ORDER_ASCENDING": "يۇقىرىغا", "LISTS_SORT_ORDER_DESCENDING": "تۆۋەنگە", "LISTS_SORT_TYPE_NUMERIC": "سان بويىچە تىزىل", "LISTS_SORT_TYPE_TEXT": "ھەرپ بويىچە تىزىل", "LISTS_SORT_TYPE_IGNORECASE": "ھەرب بويىچە تىزىل، چوڭ كىچىك يېزىلىش ھېساپ قىلىنمايدۇ", + "PROCEDURES_DEFNORETURN_TITLE": "غا", "DIALOG_OK": "ماقۇل", "DIALOG_CANCEL": "ۋاز كەچ" } diff --git a/msg/json/uk.json b/msg/json/uk.json index 6d9b4b45460..d880c758f07 100644 --- a/msg/json/uk.json +++ b/msg/json/uk.json @@ -4,6 +4,7 @@ "Andriykopanytsia", "Base", "Gzhegozh", + "Ignatgg", "Igor Zavadsky", "Lxlalexlxl", "Movses", diff --git a/msg/json/ur.json b/msg/json/ur.json index adc8bb4859f..b97806609c3 100644 --- a/msg/json/ur.json +++ b/msg/json/ur.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Aafi", "Abdulq", "NajeebKhan", "Obaid Raza", diff --git a/msg/json/xmf.json b/msg/json/xmf.json index c1ae0d980c2..3026a53355a 100644 --- a/msg/json/xmf.json +++ b/msg/json/xmf.json @@ -38,7 +38,9 @@ "NEW_VARIABLE_TITLE": "ახალი მათირეფონიშ ჯოხო:", "VARIABLE_ALREADY_EXISTS": "მათირეფონი ჯოხოთი '%1' უკვე რე.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "მათირეფონი ჯოხოთი '%1' უკვე რე შხვა ტიპიშო: '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "მათირე ჯოხოთ '%1' უკვე რე პარამეტრიშ სახეთ პროცედურას '%2'.", "DELETE_VARIABLE_CONFIRMATION": "'%2' მათირეფონიშ გჷმორინაფა %1 ბლასათო?", + "CANNOT_DELETE_VARIABLE_PROCEDURE": "ვეშილებე მათირეშ '%1' ლასუა, ანდანე თინა ფუნქციაშ '%2' გოთანჯუაშ ნორთი რე", "DELETE_VARIABLE": "'%1' მათირეფონიშ ლასუა", "COLOUR_PICKER_HELPURL": "https://xmf.wikipedia.org/wiki/ფერი", "COLOUR_PICKER_TOOLTIP": "გეგშაგორით ფერი პალიტრაშე.", @@ -63,6 +65,7 @@ "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "სოიშახ შანულობა ნანდული რე, ზოჯუეფიშ რსულება.", "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "სოიშახ შანულობა ტყურა რე, ზოჯუეფიშ რსულება", "CONTROLS_FOR_TOOLTIP": "მათირეფონი '%1'-ის მითმურჩქინანს შანულობას მოჩამილი ბიჯგეფით დუდშე ბოლოშა დო მეწურაფილ ზოჯუეფს არსულენს.", + "CONTROLS_FOREACH_TITLE": "ირი ელემენტიშო %1 ერკებულს %2", "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "ციკლშე გიშულა", "CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE": "ციკლიშ გეჸვენჯი ბიჯგშა გინულა", "CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK": "თე ციკლიშ მეჭყორიდუა.", diff --git a/msg/json/zgh.json b/msg/json/zgh.json index 33401304c15..317cf3c17fa 100644 --- a/msg/json/zgh.json +++ b/msg/json/zgh.json @@ -22,8 +22,8 @@ "RENAME_VARIABLE": "ⵙⵏⴼⵍ ⵉⵙⵎ ⵏ ⵓⵎⵙⴽⵉⵍ...", "RENAME_VARIABLE_TITLE": "ⵙⵏⴼⵍ ⵉⵎⵙⴽⵉⵍⵏ ⴰⴽⴽ '%1' ⵖⵔ:", "NEW_VARIABLE": "ⵙⴽⵔ ⴰⵎⵙⴽⵉⵍ...", - "NEW_NUMBER_VARIABLE": "ⵙⴽⵔ ⴰⵎⴹⴰⵏ ⴰⵎⵙⴽⵉⵍ...", - "NEW_COLOUR_VARIABLE": "ⵙⴽⵔ ⴰⴽⵍⵓ ⴰⵎⵙⴽⵉⵍ...", + "NEW_NUMBER_VARIABLE": "ⵙⵏⵓⵍⴼⵓ ⴰⵎⵙⴽⵉⵍ ⴰⵏⵎⴹⴰⵏ...", + "NEW_COLOUR_VARIABLE": "ⵙⵏⵓⵍⴼⵓ ⴰⵎⵙⴽⵉⵍ ⵏ ⵓⴽⵍⵓ...", "NEW_VARIABLE_TYPE_TITLE": "ⴰⵏⴰⵡ ⴰⵎⴰⵢⵏⵓ ⵏ ⵓⵎⵙⴽⵉⵍ:", "NEW_VARIABLE_TITLE": "ⵉⵙⵎ ⵏ ⵓⵎⵙⴽⵉⵍ ⴰⵎⴰⵢⵏⵓ:", "DELETE_VARIABLE": "ⴽⴽⵙ ⴰⵎⵙⴽⵉⵍ '%1'", @@ -57,7 +57,7 @@ "MATH_CHANGE_TITLE": "ⵙⵏⴼⵍ %1 ⵙ %2", "MATH_CHANGE_TOOLTIP": "ⵔⵏⵓ ⵢⴰⵏ ⵓⵎⴹⴰⵏ ⵖⵔ ⵓⵎⵙⴽⵉⵍ '%1'", "MATH_ATAN2_TITLE": "atan2 ⵙⴳ X:%1 Y:%2", - "TEXT_JOIN_TITLE_CREATEWITH": "ⵙⵏⴼⵍⵓⵍ ⴰⴹⵕⵉⵚ ⵙ", + "TEXT_JOIN_TITLE_CREATEWITH": "ⵙⵏⵓⵍⴼⵓ ⴰⴹⵕⵉⵚ ⵙ", "TEXT_CREATE_JOIN_TITLE_JOIN": "ⵍⴽⵎ", "TEXT_LENGTH_TITLE": "ⵜⵉⵖⵣⵉ ⵏ %1", "TEXT_INDEXOF_TITLE": "ⴳ ⵓⴹⵕⵉⵚ %1 %2 %3", diff --git a/msg/json/zh-hans.json b/msg/json/zh-hans.json index 7bb6b4d50a9..ccbe7733781 100644 --- a/msg/json/zh-hans.json +++ b/msg/json/zh-hans.json @@ -21,6 +21,7 @@ "Qiyue2001", "Shatteredwind", "Shimamura Sakura", + "TFX202X", "Tonylianlong", "WindWood", "Xiaomingyan", @@ -345,11 +346,12 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "做点什么", "PROCEDURES_BEFORE_PARAMS": "与:", "PROCEDURES_CALL_BEFORE_PARAMS": "与:", - "PROCEDURES_DEFNORETURN_TOOLTIP": "创建一个不带输出值的函数。", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "无法运行用户定义函数'%1',因为定义块已被禁用。", + "PROCEDURES_DEFNORETURN_TOOLTIP": "创建没有输出值的函数。", "PROCEDURES_DEFNORETURN_COMMENT": "描述该功能...", "PROCEDURES_DEFRETURN_HELPURL": "https://zh.wikipedia.org/wiki/子程序", "PROCEDURES_DEFRETURN_RETURN": "返回", - "PROCEDURES_DEFRETURN_TOOLTIP": "创建一个有输出值的函数。", + "PROCEDURES_DEFRETURN_TOOLTIP": "创建有输出值的函数。", "PROCEDURES_ALLOW_STATEMENTS": "允许声明", "PROCEDURES_DEF_DUPLICATE_WARNING": "警告:此函数具有重复参数。", "PROCEDURES_CALLNORETURN_HELPURL": "https://zh.wikipedia.org/wiki/子程序", From f9337b247993ff2ce2f940ffc79abe1e7c4bc8de Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 15 May 2025 12:59:22 -0700 Subject: [PATCH 221/222] chore: Update metadata for 2025 Q2 release (#9058) --- tests/scripts/check_metadata.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/scripts/check_metadata.sh b/tests/scripts/check_metadata.sh index 5c4eb5dcc98..321c8ea03cd 100755 --- a/tests/scripts/check_metadata.sh +++ b/tests/scripts/check_metadata.sh @@ -39,7 +39,8 @@ readonly RELEASE_DIR='dist' # Q4 2023 10.2.2 903535 # Q1 2024 10.3.1 914366 # Q2 2024 11.0.0 905365 -readonly BLOCKLY_SIZE_EXPECTED=905365 +# Q2 2025 11.2.2 922504 +readonly BLOCKLY_SIZE_EXPECTED=922504 # Size of blocks_compressed.js # Q2 2019 2.20190722.0 75618 @@ -64,7 +65,8 @@ readonly BLOCKLY_SIZE_EXPECTED=905365 # Q4 2023 10.2.2 90269 # Q1 2024 10.3.1 90269 # Q2 2024 11.0.0 88376 -readonly BLOCKS_SIZE_EXPECTED=88376 +# Q2 2025 11.2.2 88845 +readonly BLOCKS_SIZE_EXPECTED=88845 # Size of blockly_compressed.js.gz # Q2 2019 2.20190722.0 180925 @@ -90,7 +92,8 @@ readonly BLOCKS_SIZE_EXPECTED=88376 # Q4 2023 10.2.2 181474 # Q1 2024 10.3.1 184237 # Q2 2024 11.0.0 182249 -readonly BLOCKLY_GZ_SIZE_EXPECTED=182249 +# Q2 2025 11.2.2 185336 +readonly BLOCKLY_GZ_SIZE_EXPECTED=185336 # Size of blocks_compressed.js.gz # Q2 2019 2.20190722.0 14552 @@ -115,7 +118,8 @@ readonly BLOCKLY_GZ_SIZE_EXPECTED=182249 # Q4 2023 10.2.2 16442 # Q1 2024 10.3.1 16533 # Q2 2024 11.0.0 15815 -readonly BLOCKS_GZ_SIZE_EXPECTED=15815 +# Q2 2025 11.2.2 15887 +readonly BLOCKS_GZ_SIZE_EXPECTED=15887 # ANSI colors readonly BOLD_GREEN='\033[1;32m' From e5de970178659e25cd3a875d82b9183de471c7d7 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 15 May 2025 13:00:58 -0700 Subject: [PATCH 222/222] release: Update version number to 12.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b90c92c41e..684f30e0681 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockly", - "version": "12.0.0-beta.7", + "version": "12.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "12.0.0-beta.7", + "version": "12.0.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 1de4129b99a..5407f21afd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "12.0.0-beta.7", + "version": "12.0.0", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly"