From 0c2470183cf857d22f05333d9221cde8db3ff97c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 26 Jun 2024 11:19:21 -0700 Subject: [PATCH 001/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] =?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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] 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/151] =?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 0f07567965955db76a05b0295a56c78679489dd5 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 20 Mar 2025 09:46:31 -0700 Subject: [PATCH 104/151] 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 105/151] 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 106/151] 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 516e3af936b2ba0cba511dc22378d4361be96d80 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Mar 2025 21:57:30 +0000 Subject: [PATCH 107/151] 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 108/151] 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 109/151] 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 110/151] 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 717135099205a482a85eaafceaa1e4edf67ab125 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 1 Apr 2025 14:59:40 -0700 Subject: [PATCH 111/151] 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 112/151] 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 113/151] 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 114/151] 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 115/151] 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 116/151] 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 117/151] 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 118/151] 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 119/151] 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 2c05119ef28c9071f82f0c2c5cb225c39318ab6b Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Tue, 8 Apr 2025 12:06:05 -0700 Subject: [PATCH 120/151] 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 121/151] 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 122/151] 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 123/151] 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 124/151] 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 fac75043dde579206a3163c2a134337afcd687ae Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Mon, 14 Apr 2025 09:58:58 -0700 Subject: [PATCH 125/151] 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 126/151] 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 127/151] 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 128/151] 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 129/151] 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 130/151] 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 131/151] 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 132/151] 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 133/151] 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 296ad33c21e316920831612e82abc9e96344d457 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 17 Apr 2025 09:48:17 -0700 Subject: [PATCH 134/151] 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 5b103e1d7922b2d8dab432b72762b421e34aa3e1 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 17 Apr 2025 15:39:04 -0700 Subject: [PATCH 135/151] 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 136/151] 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 137/151] 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 9d127698d6e30b101a8554e6ff0618341314b1e1 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 21 Apr 2025 10:44:57 -0700 Subject: [PATCH 138/151] 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 139/151] 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 140/151] 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 141/151] 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 142/151] 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 c6e58c4f92dece0b3da241e0cc31ea09f922b7ce Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 21 Apr 2025 15:32:45 -0700 Subject: [PATCH 143/151] 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 144/151] 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 2564239d23437a133b33209c03bcad7e57848365 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 23 Apr 2025 21:22:37 +0000 Subject: [PATCH 145/151] 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 146/151] 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 147/151] 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 148/151] 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 149/151] 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 023358ee11fbc9ad760f22a8aeb51cd42c1fa6cb Mon Sep 17 00:00:00 2001 From: Erik Pasternak Date: Fri, 25 Apr 2025 14:32:21 -0700 Subject: [PATCH 150/151] Revert "feat: Make toolbox and flyout focusable (#8920)" This reverts commit 5bc83808bfe119a98de917f08840997dc9d3c324. --- 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, 7 insertions(+), 194 deletions(-) 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/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/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..b1c96373771 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2690,19 +2690,7 @@ export class WorkspaceSvg ): 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); - } - } + onTreeBlur(_nextTree: IFocusableTree | null): void {} } /** 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 15ced80aa4211d8222494ea05a67dd421890143e Mon Sep 17 00:00:00 2001 From: Erik Pasternak Date: Fri, 25 Apr 2025 14:37:08 -0700 Subject: [PATCH 151/151] Revert "feat: Make WorkspaceSvg and BlockSvg focusable (#8916)" This reverts commit d7680cf32ec0fff5b1fabf8d786f996e9e454ff2. --- core/block_svg.ts | 28 +--------- core/inject.ts | 5 ++ core/renderers/common/path_object.ts | 2 +- core/workspace_svg.ts | 77 +--------------------------- 4 files changed, 9 insertions(+), 103 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/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/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/workspace_svg.ts b/core/workspace_svg.ts index b1c96373771..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,55 +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 {} } /**