mirror of
https://github.com/google/blockly.git
synced 2026-01-10 02:17:09 +01:00
release: Merge branch 'develop' into rc/v12.3.0
This commit is contained in:
4
.github/workflows/appengine_deploy.yml
vendored
4
.github/workflows/appengine_deploy.yml
vendored
@@ -36,13 +36,13 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Download prepared files
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: appengine_files
|
||||
path: _deploy/
|
||||
|
||||
- name: Deploy to App Engine
|
||||
uses: google-github-actions/deploy-appengine@v2.1.5
|
||||
uses: google-github-actions/deploy-appengine@v2.1.7
|
||||
# For parameters see:
|
||||
# https://github.com/google-github-actions/deploy-appengine#inputs
|
||||
with:
|
||||
|
||||
4
.github/workflows/browser_test.yml
vendored
4
.github/workflows/browser_test.yml
vendored
@@ -5,13 +5,15 @@ name: Run browser manually
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Runs every Monday at 06:00 UTC
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 120
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
- uses: actions/first-interaction@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-message: >
|
||||
|
||||
@@ -501,22 +501,32 @@ export class Block {
|
||||
// Detach this block from the parent's tree.
|
||||
this.previousConnection.disconnect();
|
||||
}
|
||||
const nextBlock = this.getNextBlock();
|
||||
if (opt_healStack && nextBlock && !nextBlock.isShadow()) {
|
||||
// Disconnect the next statement.
|
||||
const nextTarget = this.nextConnection?.targetConnection ?? null;
|
||||
nextTarget?.disconnect();
|
||||
if (
|
||||
previousTarget &&
|
||||
this.workspace.connectionChecker.canConnect(
|
||||
previousTarget,
|
||||
nextTarget,
|
||||
false,
|
||||
)
|
||||
) {
|
||||
// Attach the next statement to the previous statement.
|
||||
previousTarget.connect(nextTarget!);
|
||||
}
|
||||
|
||||
if (!opt_healStack) return;
|
||||
|
||||
// Immovable or shadow next blocks need to move along with the block; keep
|
||||
// going until we encounter a normal block or run off the end of the stack.
|
||||
let nextBlock = this.getNextBlock();
|
||||
while (nextBlock && (nextBlock.isShadow() || !nextBlock.isMovable())) {
|
||||
nextBlock = nextBlock.getNextBlock();
|
||||
}
|
||||
if (!nextBlock) return;
|
||||
|
||||
// Disconnect the next statement.
|
||||
const nextTarget =
|
||||
nextBlock.previousConnection?.targetBlock()?.nextConnection
|
||||
?.targetConnection ?? null;
|
||||
nextTarget?.disconnect();
|
||||
if (
|
||||
previousTarget &&
|
||||
this.workspace.connectionChecker.canConnect(
|
||||
previousTarget,
|
||||
nextTarget,
|
||||
false,
|
||||
)
|
||||
) {
|
||||
// Attach the next statement to the previous statement.
|
||||
previousTarget.connect(nextTarget!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1118,7 +1128,7 @@ export class Block {
|
||||
*
|
||||
* @yields A generator that can be used to iterate the fields on the block.
|
||||
*/
|
||||
*getFields(): Generator<Field> {
|
||||
*getFields(): Generator<Field, undefined, void> {
|
||||
for (const input of this.inputList) {
|
||||
for (const field of input.fieldRow) {
|
||||
yield field;
|
||||
|
||||
@@ -951,9 +951,12 @@ export class BlockSvg
|
||||
/**
|
||||
* Encode a block for copying.
|
||||
*
|
||||
* @param addNextBlocks If true, copy subsequent blocks attached to this one
|
||||
* as well.
|
||||
*
|
||||
* @returns Copy metadata, or null if the block is an insertion marker.
|
||||
*/
|
||||
toCopyData(): BlockCopyData | null {
|
||||
toCopyData(addNextBlocks = false): BlockCopyData | null {
|
||||
if (this.isInsertionMarker_) {
|
||||
return null;
|
||||
}
|
||||
@@ -961,7 +964,8 @@ export class BlockSvg
|
||||
paster: BlockPaster.TYPE,
|
||||
blockState: blocks.save(this, {
|
||||
addCoordinates: true,
|
||||
addNextBlocks: false,
|
||||
addNextBlocks,
|
||||
saveIds: false,
|
||||
}) as blocks.State,
|
||||
typeCounts: common.getBlockTypeCounts(this, true),
|
||||
};
|
||||
|
||||
@@ -9,7 +9,9 @@ import * as common from '../common.js';
|
||||
import {BubbleDragStrategy} from '../dragging/bubble_drag_strategy.js';
|
||||
import {getFocusManager} from '../focus_manager.js';
|
||||
import {IBubble} from '../interfaces/i_bubble.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {IFocusableTree} from '../interfaces/i_focusable_tree.js';
|
||||
import type {IHasBubble} from '../interfaces/i_has_bubble.js';
|
||||
import {ISelectable} from '../interfaces/i_selectable.js';
|
||||
import {ContainerRegion} from '../metrics_manager.js';
|
||||
import {Scrollbar} from '../scrollbar.js';
|
||||
@@ -27,7 +29,7 @@ import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
* bubble, where it has a "tail" that points to the block, and a "head" that
|
||||
* displays arbitrary svg elements.
|
||||
*/
|
||||
export abstract class Bubble implements IBubble, ISelectable {
|
||||
export abstract class Bubble implements IBubble, ISelectable, IFocusableNode {
|
||||
/** The width of the border around the bubble. */
|
||||
static readonly BORDER_WIDTH = 6;
|
||||
|
||||
@@ -100,12 +102,14 @@ export abstract class Bubble implements IBubble, ISelectable {
|
||||
* element that's represented by this bubble (as a focusable node). This
|
||||
* element will have its ID overwritten. If not provided, the focusable
|
||||
* element of this node will default to the bubble's SVG root.
|
||||
* @param owner The object responsible for hosting/spawning this bubble.
|
||||
*/
|
||||
constructor(
|
||||
public readonly workspace: WorkspaceSvg,
|
||||
protected anchor: Coordinate,
|
||||
protected ownerRect?: Rect,
|
||||
overriddenFocusableElement?: SVGElement | HTMLElement,
|
||||
protected owner?: IHasBubble & IFocusableNode,
|
||||
) {
|
||||
this.id = idGenerator.getNextUniqueId();
|
||||
this.svgRoot = dom.createSvgElement(
|
||||
@@ -145,6 +149,13 @@ export abstract class Bubble implements IBubble, ISelectable {
|
||||
this,
|
||||
this.onMouseDown,
|
||||
);
|
||||
|
||||
browserEvents.conditionalBind(
|
||||
this.focusableElement,
|
||||
'keydown',
|
||||
this,
|
||||
this.onKeyDown,
|
||||
);
|
||||
}
|
||||
|
||||
/** Dispose of this bubble. */
|
||||
@@ -229,6 +240,19 @@ export abstract class Bubble implements IBubble, ISelectable {
|
||||
getFocusManager().focusNode(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles key events when this bubble is focused. By default, closes the
|
||||
* bubble on Escape.
|
||||
*
|
||||
* @param e The keyboard event to handle.
|
||||
*/
|
||||
protected onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && this.owner) {
|
||||
this.owner.setBubbleVisible(false);
|
||||
getFocusManager().focusNode(this.owner);
|
||||
}
|
||||
}
|
||||
|
||||
/** Positions the bubble relative to its anchor. Does not render its tail. */
|
||||
protected positionRelativeToAnchor() {
|
||||
let left = this.anchor.x;
|
||||
@@ -694,4 +718,11 @@ export abstract class Bubble implements IBubble, ISelectable {
|
||||
canBeFocused(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object that owns/hosts this bubble, if any.
|
||||
*/
|
||||
getOwner(): (IHasBubble & IFocusableNode) | undefined {
|
||||
return this.owner;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {CommentEditor} from '../comments/comment_editor.js';
|
||||
import * as Css from '../css.js';
|
||||
import {getFocusManager} from '../focus_manager.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {IHasBubble} from '../interfaces/i_has_bubble.js';
|
||||
import * as touch from '../touch.js';
|
||||
import {browserEvents} from '../utils.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
@@ -21,12 +25,6 @@ import {Bubble} from './bubble.js';
|
||||
* Used by the comment icon.
|
||||
*/
|
||||
export class TextInputBubble extends Bubble {
|
||||
/** The root of the elements specific to the text element. */
|
||||
private inputRoot: SVGForeignObjectElement;
|
||||
|
||||
/** The text input area element. */
|
||||
private textArea: HTMLTextAreaElement;
|
||||
|
||||
/** The group containing the lines indicating the bubble is resizable. */
|
||||
private resizeGroup: SVGGElement;
|
||||
|
||||
@@ -42,18 +40,12 @@ export class TextInputBubble extends Bubble {
|
||||
*/
|
||||
private resizePointerMoveListener: browserEvents.Data | null = null;
|
||||
|
||||
/** Functions listening for changes to the text of this bubble. */
|
||||
private textChangeListeners: (() => void)[] = [];
|
||||
|
||||
/** 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 = '';
|
||||
|
||||
/** The default size of this bubble, including borders. */
|
||||
private readonly DEFAULT_SIZE = new Size(
|
||||
160 + Bubble.DOUBLE_BORDER,
|
||||
@@ -68,46 +60,47 @@ export class TextInputBubble extends Bubble {
|
||||
|
||||
private editable = true;
|
||||
|
||||
/** View responsible for supporting text editing. */
|
||||
private editor: CommentEditor;
|
||||
|
||||
/**
|
||||
* @param workspace The workspace this bubble belongs to.
|
||||
* @param anchor The anchor location of the thing this bubble is attached to.
|
||||
* The tail of the bubble will point to this location.
|
||||
* @param ownerRect An optional rect we don't want the bubble to overlap with
|
||||
* when automatically positioning.
|
||||
* @param owner The object that owns/hosts this bubble.
|
||||
*/
|
||||
constructor(
|
||||
public readonly workspace: WorkspaceSvg,
|
||||
protected anchor: Coordinate,
|
||||
protected ownerRect?: Rect,
|
||||
protected owner?: IHasBubble & IFocusableNode,
|
||||
) {
|
||||
super(workspace, anchor, ownerRect, TextInputBubble.createTextArea());
|
||||
super(workspace, anchor, ownerRect, undefined, owner);
|
||||
dom.addClass(this.svgRoot, 'blocklyTextInputBubble');
|
||||
this.textArea = this.getFocusableElement() as HTMLTextAreaElement;
|
||||
this.inputRoot = this.createEditor(this.contentContainer, this.textArea);
|
||||
this.editor = new CommentEditor(workspace, this.id, () => {
|
||||
getFocusManager().focusNode(this);
|
||||
});
|
||||
this.contentContainer.appendChild(this.editor.getDom());
|
||||
this.resizeGroup = this.createResizeHandle(this.svgRoot, workspace);
|
||||
this.setSize(this.DEFAULT_SIZE, true);
|
||||
}
|
||||
|
||||
/** @returns the text of this bubble. */
|
||||
getText(): string {
|
||||
return this.text;
|
||||
return this.editor.getText();
|
||||
}
|
||||
|
||||
/** Sets the text of this bubble. Calls change listeners. */
|
||||
setText(text: string) {
|
||||
this.text = text;
|
||||
this.textArea.value = text;
|
||||
this.onTextChange();
|
||||
this.editor.setText(text);
|
||||
}
|
||||
|
||||
/** Sets whether or not the text in the bubble is editable. */
|
||||
setEditable(editable: boolean) {
|
||||
this.editable = editable;
|
||||
if (this.editable) {
|
||||
this.textArea.removeAttribute('readonly');
|
||||
} else {
|
||||
this.textArea.setAttribute('readonly', '');
|
||||
}
|
||||
this.editor.setEditable(editable);
|
||||
}
|
||||
|
||||
/** Returns whether or not the text in the bubble is editable. */
|
||||
@@ -117,7 +110,7 @@ export class TextInputBubble extends Bubble {
|
||||
|
||||
/** Adds a change listener to be notified when this bubble's text changes. */
|
||||
addTextChangeListener(listener: () => void) {
|
||||
this.textChangeListeners.push(listener);
|
||||
this.editor.addTextChangeListener(listener);
|
||||
}
|
||||
|
||||
/** Adds a change listener to be notified when this bubble's size changes. */
|
||||
@@ -130,58 +123,6 @@ export class TextInputBubble extends Bubble {
|
||||
this.locationChangeListeners.push(listener);
|
||||
}
|
||||
|
||||
/** Creates and returns the editable text area for this bubble's editor. */
|
||||
private static createTextArea(): HTMLTextAreaElement {
|
||||
const textArea = document.createElementNS(
|
||||
dom.HTML_NS,
|
||||
'textarea',
|
||||
) as HTMLTextAreaElement;
|
||||
textArea.className = 'blocklyTextarea blocklyText';
|
||||
return textArea;
|
||||
}
|
||||
|
||||
/** Creates and returns the UI container element for this bubble's editor. */
|
||||
private createEditor(
|
||||
container: SVGGElement,
|
||||
textArea: HTMLTextAreaElement,
|
||||
): SVGForeignObjectElement {
|
||||
const inputRoot = dom.createSvgElement(
|
||||
Svg.FOREIGNOBJECT,
|
||||
{
|
||||
'x': Bubble.BORDER_WIDTH,
|
||||
'y': Bubble.BORDER_WIDTH,
|
||||
},
|
||||
container,
|
||||
);
|
||||
|
||||
const body = document.createElementNS(dom.HTML_NS, 'body');
|
||||
body.setAttribute('xmlns', dom.HTML_NS);
|
||||
body.className = 'blocklyMinimalBody';
|
||||
|
||||
textArea.setAttribute('dir', this.workspace.RTL ? 'RTL' : 'LTR');
|
||||
body.appendChild(textArea);
|
||||
inputRoot.appendChild(body);
|
||||
|
||||
this.bindTextAreaEvents(textArea);
|
||||
|
||||
return inputRoot;
|
||||
}
|
||||
|
||||
/** Binds events to the text area element. */
|
||||
private bindTextAreaEvents(textArea: HTMLTextAreaElement) {
|
||||
// Don't zoom with mousewheel; let it scroll instead.
|
||||
browserEvents.conditionalBind(textArea, 'wheel', this, (e: Event) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
// Don't let the pointerdown event get to the workspace.
|
||||
browserEvents.conditionalBind(textArea, 'pointerdown', this, (e: Event) => {
|
||||
e.stopPropagation();
|
||||
touch.clearTouchIdentifier();
|
||||
});
|
||||
|
||||
browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange);
|
||||
}
|
||||
|
||||
/** Creates the resize handler elements and binds events to them. */
|
||||
private createResizeHandle(
|
||||
container: SVGGElement,
|
||||
@@ -220,8 +161,12 @@ export class TextInputBubble extends Bubble {
|
||||
|
||||
const widthMinusBorder = size.width - Bubble.DOUBLE_BORDER;
|
||||
const heightMinusBorder = size.height - Bubble.DOUBLE_BORDER;
|
||||
this.inputRoot.setAttribute('width', `${widthMinusBorder}`);
|
||||
this.inputRoot.setAttribute('height', `${heightMinusBorder}`);
|
||||
this.editor.updateSize(
|
||||
new Size(widthMinusBorder, heightMinusBorder),
|
||||
new Size(0, 0),
|
||||
);
|
||||
this.editor.getDom().setAttribute('x', `${Bubble.DOUBLE_BORDER / 2}`);
|
||||
this.editor.getDom().setAttribute('y', `${Bubble.DOUBLE_BORDER / 2}`);
|
||||
|
||||
this.resizeGroup.setAttribute('y', `${heightMinusBorder}`);
|
||||
if (this.workspace.RTL) {
|
||||
@@ -312,14 +257,6 @@ export class TextInputBubble extends Bubble {
|
||||
this.onSizeChange();
|
||||
}
|
||||
|
||||
/** Handles a text change event for the text area. Calls event listeners. */
|
||||
private onTextChange() {
|
||||
this.text = this.textArea.value;
|
||||
for (const listener of this.textChangeListeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles a size change event for the text area. Calls event listeners. */
|
||||
private onSizeChange() {
|
||||
for (const listener of this.sizeChangeListeners) {
|
||||
@@ -333,6 +270,15 @@ export class TextInputBubble extends Bubble {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text editor component of this bubble.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
getEditor() {
|
||||
return this.editor;
|
||||
}
|
||||
}
|
||||
|
||||
Css.register(`
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as dom from '../utils/dom.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {CommentBarButton} from './comment_bar_button.js';
|
||||
import type {CommentView} from './comment_view.js';
|
||||
|
||||
/**
|
||||
* Magic string appended to the comment ID to create a unique ID for this button.
|
||||
@@ -42,8 +43,9 @@ export class CollapseCommentBarButton extends CommentBarButton {
|
||||
protected readonly id: string,
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
protected readonly container: SVGGElement,
|
||||
protected readonly commentView: CommentView,
|
||||
) {
|
||||
super(id, workspace, container);
|
||||
super(id, workspace, container, commentView);
|
||||
|
||||
this.icon = dom.createSvgElement(
|
||||
Svg.IMAGE,
|
||||
@@ -86,14 +88,13 @@ export class CollapseCommentBarButton extends CommentBarButton {
|
||||
override performAction(e?: Event) {
|
||||
touch.clearTouchIdentifier();
|
||||
|
||||
const comment = this.getParentComment();
|
||||
comment.view.bringToFront();
|
||||
this.getCommentView().bringToFront();
|
||||
if (e && e instanceof PointerEvent && browserEvents.isRightButton(e)) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
comment.setCollapsed(!comment.isCollapsed());
|
||||
this.getCommentView().setCollapsed(!this.getCommentView().isCollapsed());
|
||||
this.workspace.hideChaff();
|
||||
|
||||
e?.stopPropagation();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import {Rect} from '../utils/rect.js';
|
||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import type {RenderedWorkspaceComment} from './rendered_workspace_comment.js';
|
||||
import type {CommentView} from './comment_view.js';
|
||||
|
||||
/**
|
||||
* Button displayed on a comment's top bar.
|
||||
@@ -29,6 +29,7 @@ export abstract class CommentBarButton implements IFocusableNode {
|
||||
protected readonly id: string,
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
protected readonly container: SVGGElement,
|
||||
protected readonly commentView: CommentView,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -39,17 +40,10 @@ export abstract class CommentBarButton implements IFocusableNode {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent comment of this comment bar button.
|
||||
* Returns the parent comment view of this comment bar button.
|
||||
*/
|
||||
getParentComment(): RenderedWorkspaceComment {
|
||||
const comment = this.workspace.getCommentById(this.id);
|
||||
if (!comment) {
|
||||
throw new Error(
|
||||
`Comment bar button ${this.id} has no corresponding comment`,
|
||||
);
|
||||
}
|
||||
|
||||
return comment;
|
||||
getCommentView(): CommentView {
|
||||
return this.commentView;
|
||||
}
|
||||
|
||||
/** Adjusts the position of this button within its parent container. */
|
||||
|
||||
@@ -53,6 +53,7 @@ export class CommentEditor implements IFocusableNode {
|
||||
'textarea',
|
||||
) as HTMLTextAreaElement;
|
||||
this.textArea.setAttribute('tabindex', '-1');
|
||||
this.textArea.setAttribute('dir', this.workspace.RTL ? 'RTL' : 'LTR');
|
||||
dom.addClass(this.textArea, 'blocklyCommentText');
|
||||
dom.addClass(this.textArea, 'blocklyTextarea');
|
||||
dom.addClass(this.textArea, 'blocklyText');
|
||||
@@ -86,6 +87,11 @@ export class CommentEditor implements IFocusableNode {
|
||||
},
|
||||
);
|
||||
|
||||
// Don't zoom with mousewheel; let it scroll instead.
|
||||
browserEvents.conditionalBind(this.textArea, 'wheel', this, (e: Event) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Register listener for keydown events that would finish editing.
|
||||
browserEvents.conditionalBind(
|
||||
this.textArea,
|
||||
|
||||
@@ -102,7 +102,7 @@ export class CommentView implements IRenderedElement {
|
||||
|
||||
constructor(
|
||||
readonly workspace: WorkspaceSvg,
|
||||
private commentId: string,
|
||||
readonly commentId: string,
|
||||
) {
|
||||
this.svgRoot = dom.createSvgElement(Svg.G, {
|
||||
'class': 'blocklyComment blocklyEditable blocklyDraggable',
|
||||
@@ -176,12 +176,18 @@ export class CommentView implements IRenderedElement {
|
||||
this.commentId,
|
||||
this.workspace,
|
||||
topBarGroup,
|
||||
this,
|
||||
);
|
||||
const foldoutButton = new CollapseCommentBarButton(
|
||||
this.commentId,
|
||||
this.workspace,
|
||||
topBarGroup,
|
||||
this,
|
||||
);
|
||||
this.addDisposeListener(() => {
|
||||
deleteButton.dispose();
|
||||
foldoutButton.dispose();
|
||||
});
|
||||
const textPreview = dom.createSvgElement(
|
||||
Svg.TEXT,
|
||||
{
|
||||
@@ -612,13 +618,12 @@ export class CommentView implements IRenderedElement {
|
||||
/** Disposes of this comment view. */
|
||||
dispose() {
|
||||
this.disposing = true;
|
||||
this.foldoutButton.dispose();
|
||||
this.deleteButton.dispose();
|
||||
dom.removeNode(this.svgRoot);
|
||||
// Loop through listeners backwards in case they remove themselves.
|
||||
for (let i = this.disposeListeners.length - 1; i >= 0; i--) {
|
||||
this.disposeListeners[i]();
|
||||
}
|
||||
this.disposeListeners.length = 0;
|
||||
this.disposed = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as dom from '../utils/dom.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {CommentBarButton} from './comment_bar_button.js';
|
||||
import type {CommentView} from './comment_view.js';
|
||||
|
||||
/**
|
||||
* Magic string appended to the comment ID to create a unique ID for this button.
|
||||
@@ -42,8 +43,9 @@ export class DeleteCommentBarButton extends CommentBarButton {
|
||||
protected readonly id: string,
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
protected readonly container: SVGGElement,
|
||||
protected readonly commentView: CommentView,
|
||||
) {
|
||||
super(id, workspace, container);
|
||||
super(id, workspace, container, commentView);
|
||||
|
||||
this.icon = dom.createSvgElement(
|
||||
Svg.IMAGE,
|
||||
@@ -97,7 +99,7 @@ export class DeleteCommentBarButton extends CommentBarButton {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getParentComment().dispose();
|
||||
this.getCommentView().dispose();
|
||||
e?.stopPropagation();
|
||||
getFocusManager().focusNode(this.workspace);
|
||||
}
|
||||
|
||||
@@ -74,15 +74,6 @@ export class RenderedWorkspaceComment
|
||||
this,
|
||||
this.startGesture,
|
||||
);
|
||||
// Don't zoom with mousewheel; let it scroll instead.
|
||||
browserEvents.conditionalBind(
|
||||
this.view.getSvgRoot(),
|
||||
'wheel',
|
||||
this,
|
||||
(e: Event) => {
|
||||
e.stopPropagation();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,6 +280,7 @@ export class RenderedWorkspaceComment
|
||||
paster: WorkspaceCommentPaster.TYPE,
|
||||
commentState: commentSerialization.save(this, {
|
||||
addCoordinates: true,
|
||||
saveIds: false,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,8 +23,15 @@ import {CommentIcon} from './icons/comment_icon.js';
|
||||
import {Msg} from './msg.js';
|
||||
import {StatementInput} from './renderers/zelos/zelos.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import * as svgMath from './utils/svg_math.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
function isFullBlockField(block?: BlockSvg) {
|
||||
if (!block || !block.isSimpleReporter()) return false;
|
||||
const firstField = block.getFields().next().value;
|
||||
return firstField?.isFullBlockField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Option to undo previous action.
|
||||
*/
|
||||
@@ -362,10 +369,15 @@ export function registerComment() {
|
||||
preconditionFn(scope: Scope) {
|
||||
const block = scope.block;
|
||||
if (
|
||||
!block!.isInFlyout &&
|
||||
block!.workspace.options.comments &&
|
||||
!block!.isCollapsed() &&
|
||||
block!.isEditable()
|
||||
block &&
|
||||
!block.isInFlyout &&
|
||||
block.workspace.options.comments &&
|
||||
!block.isCollapsed() &&
|
||||
block.isEditable() &&
|
||||
// Either block already has a comment so let us remove it,
|
||||
// or the block isn't just one full-block field block, which
|
||||
// shouldn't be allowed to have comments as there's no way to read them.
|
||||
(block.hasIcon(CommentIcon.TYPE) || !isFullBlockField(block))
|
||||
) {
|
||||
return 'enabled';
|
||||
}
|
||||
@@ -373,8 +385,8 @@ export function registerComment() {
|
||||
},
|
||||
callback(scope: Scope) {
|
||||
const block = scope.block;
|
||||
if (block!.hasIcon(CommentIcon.TYPE)) {
|
||||
block!.setCommentText(null);
|
||||
if (block && block.hasIcon(CommentIcon.TYPE)) {
|
||||
block.setCommentText(null);
|
||||
} else {
|
||||
block!.setCommentText('');
|
||||
}
|
||||
@@ -626,9 +638,9 @@ export function registerCommentCreate() {
|
||||
const comment = new RenderedWorkspaceComment(workspace);
|
||||
comment.setPlaceholderText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']);
|
||||
comment.moveTo(
|
||||
pixelsToWorkspaceCoords(
|
||||
new Coordinate(location.x, location.y),
|
||||
svgMath.screenToWsCoordinates(
|
||||
workspace,
|
||||
new Coordinate(location.x, location.y),
|
||||
),
|
||||
);
|
||||
getFocusManager().focusNode(comment);
|
||||
@@ -641,40 +653,6 @@ export function registerCommentCreate() {
|
||||
ContextMenuRegistry.registry.register(createOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts pixel coordinates (relative to the window) to workspace coordinates.
|
||||
*/
|
||||
function pixelsToWorkspaceCoords(
|
||||
pixelCoord: Coordinate,
|
||||
workspace: WorkspaceSvg,
|
||||
): Coordinate {
|
||||
const injectionDiv = workspace.getInjectionDiv();
|
||||
// Bounding rect coordinates are in client coordinates, meaning that they
|
||||
// are in pixels relative to the upper left corner of the visible browser
|
||||
// window. These coordinates change when you scroll the browser window.
|
||||
const boundingRect = injectionDiv.getBoundingClientRect();
|
||||
|
||||
// The client coordinates offset by the injection div's upper left corner.
|
||||
const clientOffsetPixels = new Coordinate(
|
||||
pixelCoord.x - boundingRect.left,
|
||||
pixelCoord.y - boundingRect.top,
|
||||
);
|
||||
|
||||
// The offset in pixels between the main workspace's origin and the upper
|
||||
// left corner of the injection div.
|
||||
const mainOffsetPixels = workspace.getOriginOffsetInPixels();
|
||||
|
||||
// The position of the new comment in pixels relative to the origin of the
|
||||
// main workspace.
|
||||
const finalOffset = Coordinate.difference(
|
||||
clientOffsetPixels,
|
||||
mainOffsetPixels,
|
||||
);
|
||||
// The position of the new comment in main workspace coordinates.
|
||||
finalOffset.scale(1 / workspace.scale);
|
||||
return finalOffset;
|
||||
}
|
||||
|
||||
/** Registers all block-scoped context menu items. */
|
||||
function registerBlockOptions_() {
|
||||
registerDuplicate();
|
||||
|
||||
@@ -241,7 +241,7 @@ let content = `
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.blocklyIconGroup:not(:hover),
|
||||
.blocklyIconGroup:not(:hover):not(:focus),
|
||||
.blocklyIconGroupReadonly {
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
@@ -699,25 +699,30 @@ export class FieldDropdown extends Field<string> {
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
} {
|
||||
let hasImages = false;
|
||||
let hasNonTextContent = false;
|
||||
const trimmedOptions = options.map((option): MenuOption => {
|
||||
if (option === FieldDropdown.SEPARATOR) return option;
|
||||
if (option === FieldDropdown.SEPARATOR) {
|
||||
hasNonTextContent = true;
|
||||
return option;
|
||||
}
|
||||
|
||||
const [label, value] = option;
|
||||
if (typeof label === 'string') {
|
||||
return [parsing.replaceMessageReferences(label), value];
|
||||
}
|
||||
|
||||
hasImages = true;
|
||||
hasNonTextContent = 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 = isImageProperties(label)
|
||||
? {...label, alt: parsing.replaceMessageReferences(label.alt)}
|
||||
: {...label};
|
||||
: label;
|
||||
return [imageLabel, value];
|
||||
});
|
||||
|
||||
if (hasImages || options.length < 2) return {options: trimmedOptions};
|
||||
if (hasNonTextContent || options.length < 2) {
|
||||
return {options: trimmedOptions};
|
||||
}
|
||||
|
||||
const stringOptions = trimmedOptions as [string, string][];
|
||||
const stringLabels = stringOptions.map(([label]) => label);
|
||||
@@ -793,7 +798,7 @@ export class FieldDropdown extends Field<string> {
|
||||
} else if (typeof option[1] !== 'string') {
|
||||
foundError = true;
|
||||
console.error(
|
||||
`Invalid option[${i}]: Each FieldDropdown option id must be a string.
|
||||
`Invalid option[${i}]: Each FieldDropdown option id must be a string.
|
||||
Found ${option[1]} in: ${option}`,
|
||||
);
|
||||
} else if (
|
||||
@@ -806,7 +811,7 @@ export class FieldDropdown extends Field<string> {
|
||||
) {
|
||||
foundError = true;
|
||||
console.error(
|
||||
`Invalid option[${i}]: Each FieldDropdown option must have a string
|
||||
`Invalid option[${i}]: Each FieldDropdown option must have a string
|
||||
label, image description, or HTML element. Found ${option[0]} in: ${option}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import type {BlockSvg} from '../block_svg.js';
|
||||
import {TextInputBubble} from '../bubbles/textinput_bubble.js';
|
||||
import {EventType} from '../events/type.js';
|
||||
import * as eventUtils from '../events/utils.js';
|
||||
import type {IBubble} from '../interfaces/i_bubble.js';
|
||||
import type {IHasBubble} from '../interfaces/i_has_bubble.js';
|
||||
import type {ISerializable} from '../interfaces/i_serializable.js';
|
||||
import * as renderManagement from '../render_management.js';
|
||||
@@ -62,7 +61,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
||||
/**
|
||||
* The visibility of the bubble for this comment.
|
||||
*
|
||||
* This is used to track what the visibile state /should/ be, not necessarily
|
||||
* This is used to track what the visible state /should/ be, not necessarily
|
||||
* what it currently /is/. E.g. sometimes this will be true, but the block
|
||||
* hasn't been rendered yet, so the bubble will not currently be visible.
|
||||
*/
|
||||
@@ -340,7 +339,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
||||
}
|
||||
|
||||
/** See IHasBubble.getBubble. */
|
||||
getBubble(): IBubble | null {
|
||||
getBubble(): TextInputBubble | null {
|
||||
return this.textInputBubble;
|
||||
}
|
||||
|
||||
@@ -365,6 +364,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
||||
this.sourceBlock.workspace as WorkspaceSvg,
|
||||
this.getAnchorLocation(),
|
||||
this.getBubbleOwnerRect(),
|
||||
this,
|
||||
);
|
||||
this.textInputBubble.setText(this.getText());
|
||||
this.textInputBubble.setSize(this.bubbleSize, true);
|
||||
|
||||
@@ -150,8 +150,17 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer {
|
||||
return markerConn;
|
||||
}
|
||||
|
||||
private createInsertionMarker(origBlock: BlockSvg) {
|
||||
const blockJson = blocks.save(origBlock, {
|
||||
/**
|
||||
* Transforms the given block into a JSON representation used to construct an
|
||||
* insertion marker.
|
||||
*
|
||||
* @param block The block to serialize and use as an insertion marker.
|
||||
* @returns A JSON-formatted string corresponding to a serialized
|
||||
* representation of the given block suitable for use as an insertion
|
||||
* marker.
|
||||
*/
|
||||
protected serializeBlockToInsertionMarker(block: BlockSvg) {
|
||||
const blockJson = blocks.save(block, {
|
||||
addCoordinates: false,
|
||||
addInputBlocks: false,
|
||||
addNextBlocks: false,
|
||||
@@ -160,10 +169,15 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer {
|
||||
|
||||
if (!blockJson) {
|
||||
throw new Error(
|
||||
`Failed to serialize source block. ${origBlock.toDevString()}`,
|
||||
`Failed to serialize source block. ${block.toDevString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
return blockJson;
|
||||
}
|
||||
|
||||
private createInsertionMarker(origBlock: BlockSvg) {
|
||||
const blockJson = this.serializeBlockToInsertionMarker(origBlock);
|
||||
const result = blocks.append(blockJson, this.workspace) as BlockSvg;
|
||||
|
||||
// Turn shadow blocks that are created programmatically during
|
||||
|
||||
@@ -43,7 +43,7 @@ export interface IVariableMap<T extends IVariableModel<IVariableState>> {
|
||||
* 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;
|
||||
createVariable(name: string, type?: string, id?: string | null): T;
|
||||
|
||||
/* Adds a variable to this variable map. */
|
||||
addVariable(variable: T): void;
|
||||
|
||||
76
core/keyboard_nav/block_comment_navigation_policy.ts
Normal file
76
core/keyboard_nav/block_comment_navigation_policy.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {TextInputBubble} from '../bubbles/textinput_bubble.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
|
||||
/**
|
||||
* Set of rules controlling keyboard navigation from an TextInputBubble.
|
||||
*/
|
||||
export class BlockCommentNavigationPolicy
|
||||
implements INavigationPolicy<TextInputBubble>
|
||||
{
|
||||
/**
|
||||
* Returns the first child of the given block comment.
|
||||
*
|
||||
* @param current The block comment to return the first child of.
|
||||
* @returns The text editor of the given block comment bubble.
|
||||
*/
|
||||
getFirstChild(current: TextInputBubble): IFocusableNode | null {
|
||||
return current.getEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent of the given block comment.
|
||||
*
|
||||
* @param current The block comment to return the parent of.
|
||||
* @returns The parent block of the given block comment.
|
||||
*/
|
||||
getParent(current: TextInputBubble): IFocusableNode | null {
|
||||
return current.getOwner() ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next peer node of the given block comment.
|
||||
*
|
||||
* @param _current The block comment to find the following element of.
|
||||
* @returns Null.
|
||||
*/
|
||||
getNextSibling(_current: TextInputBubble): IFocusableNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous peer node of the given block comment.
|
||||
*
|
||||
* @param _current The block comment to find the preceding element of.
|
||||
* @returns Null.
|
||||
*/
|
||||
getPreviousSibling(_current: TextInputBubble): IFocusableNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the given block comment can be navigated to.
|
||||
*
|
||||
* @param current The instance to check for navigability.
|
||||
* @returns True if the given block comment can be focused.
|
||||
*/
|
||||
isNavigable(current: TextInputBubble): boolean {
|
||||
return current.canBeFocused();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given object can be navigated from by this policy.
|
||||
*
|
||||
* @param current The object to check if this policy applies to.
|
||||
* @returns True if the object is an TextInputBubble.
|
||||
*/
|
||||
isApplicable(current: any): current is TextInputBubble {
|
||||
return current instanceof TextInputBubble;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,9 @@ export class CommentBarButtonNavigationPolicy
|
||||
* @returns The parent comment of the given CommentBarButton.
|
||||
*/
|
||||
getParent(current: CommentBarButton): IFocusableNode | null {
|
||||
return current.getParentComment();
|
||||
return current
|
||||
.getCommentView()
|
||||
.workspace.getCommentById(current.getCommentView().commentId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,7 +43,7 @@ export class CommentBarButtonNavigationPolicy
|
||||
* @returns The next CommentBarButton, if any.
|
||||
*/
|
||||
getNextSibling(current: CommentBarButton): IFocusableNode | null {
|
||||
const children = current.getParentComment().view.getCommentBarButtons();
|
||||
const children = current.getCommentView().getCommentBarButtons();
|
||||
const currentIndex = children.indexOf(current);
|
||||
if (currentIndex >= 0 && currentIndex + 1 < children.length) {
|
||||
return children[currentIndex + 1];
|
||||
@@ -56,7 +58,7 @@ export class CommentBarButtonNavigationPolicy
|
||||
* @returns The CommentBarButton's previous CommentBarButton, if any.
|
||||
*/
|
||||
getPreviousSibling(current: CommentBarButton): IFocusableNode | null {
|
||||
const children = current.getParentComment().view.getCommentBarButtons();
|
||||
const children = current.getCommentView().getCommentBarButtons();
|
||||
const currentIndex = children.indexOf(current);
|
||||
if (currentIndex > 0) {
|
||||
return children[currentIndex - 1];
|
||||
|
||||
54
core/keyboard_nav/comment_editor_navigation_policy.ts
Normal file
54
core/keyboard_nav/comment_editor_navigation_policy.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {CommentEditor} from '../comments/comment_editor.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
|
||||
/**
|
||||
* Set of rules controlling keyboard navigation from a comment editor.
|
||||
* This is a no-op placeholder (other than isNavigable/isApplicable) since
|
||||
* comment editors handle their own navigation when editing ends.
|
||||
*/
|
||||
export class CommentEditorNavigationPolicy
|
||||
implements INavigationPolicy<CommentEditor>
|
||||
{
|
||||
getFirstChild(_current: CommentEditor): IFocusableNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getParent(_current: CommentEditor): IFocusableNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getNextSibling(_current: CommentEditor): IFocusableNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPreviousSibling(_current: CommentEditor): IFocusableNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the given comment editor can be navigated to.
|
||||
*
|
||||
* @param current The instance to check for navigability.
|
||||
* @returns False.
|
||||
*/
|
||||
isNavigable(current: CommentEditor): boolean {
|
||||
return current.canBeFocused();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given object can be navigated from by this policy.
|
||||
*
|
||||
* @param current The object to check if this policy applies to.
|
||||
* @returns True if the object is a CommentEditor.
|
||||
*/
|
||||
isApplicable(current: any): current is CommentEditor {
|
||||
return current instanceof CommentEditor;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import {getFocusManager} from '../focus_manager.js';
|
||||
import {CommentIcon} from '../icons/comment_icon.js';
|
||||
import {Icon} from '../icons/icon.js';
|
||||
import {MutatorIcon} from '../icons/mutator_icon.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
@@ -29,6 +30,12 @@ export class IconNavigationPolicy implements INavigationPolicy<Icon> {
|
||||
getFocusManager().getFocusedNode() === current
|
||||
) {
|
||||
return current.getBubble()?.getWorkspace() ?? null;
|
||||
} else if (
|
||||
current instanceof CommentIcon &&
|
||||
current.bubbleIsVisible() &&
|
||||
getFocusManager().getFocusedNode() === current
|
||||
) {
|
||||
return current.getBubble()?.getEditor() ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
*/
|
||||
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import {CommentBarButton} from '../comments/comment_bar_button.js';
|
||||
import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js';
|
||||
import {Field} from '../field.js';
|
||||
import {getFocusManager} from '../focus_manager.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {Rect} from '../utils/rect.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {Marker} from './marker.js';
|
||||
|
||||
@@ -403,6 +405,17 @@ export class LineCursor extends Marker {
|
||||
);
|
||||
} else if (newNode instanceof RenderedWorkspaceComment) {
|
||||
newNode.workspace.scrollBoundsIntoView(newNode.getBoundingRectangle());
|
||||
} else if (newNode instanceof CommentBarButton) {
|
||||
const commentView = newNode.getCommentView();
|
||||
const xy = commentView.getRelativeToSurfaceXY();
|
||||
const size = commentView.getSize();
|
||||
const bounds = new Rect(
|
||||
xy.y,
|
||||
xy.y + size.height,
|
||||
xy.x,
|
||||
xy.x + size.width,
|
||||
);
|
||||
commentView.workspace.scrollBoundsIntoView(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
|
||||
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
|
||||
import type {INavigationPolicy} from './interfaces/i_navigation_policy.js';
|
||||
import {BlockCommentNavigationPolicy} from './keyboard_nav/block_comment_navigation_policy.js';
|
||||
import {BlockNavigationPolicy} from './keyboard_nav/block_navigation_policy.js';
|
||||
import {CommentBarButtonNavigationPolicy} from './keyboard_nav/comment_bar_button_navigation_policy.js';
|
||||
import {CommentEditorNavigationPolicy} from './keyboard_nav/comment_editor_navigation_policy.js';
|
||||
import {ConnectionNavigationPolicy} from './keyboard_nav/connection_navigation_policy.js';
|
||||
import {FieldNavigationPolicy} from './keyboard_nav/field_navigation_policy.js';
|
||||
import {IconNavigationPolicy} from './keyboard_nav/icon_navigation_policy.js';
|
||||
@@ -33,6 +35,8 @@ export class Navigator {
|
||||
new IconNavigationPolicy(),
|
||||
new WorkspaceCommentNavigationPolicy(),
|
||||
new CommentBarButtonNavigationPolicy(),
|
||||
new BlockCommentNavigationPolicy(),
|
||||
new CommentEditorNavigationPolicy(),
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -119,9 +119,9 @@ export class Type<_T> {
|
||||
/** @internal */
|
||||
static PASTER = new Type<IPaster<ICopyData, ICopyable<ICopyData>>>('paster');
|
||||
|
||||
static VARIABLE_MODEL = new Type<IVariableModelStatic<IVariableState>>(
|
||||
'variableModel',
|
||||
);
|
||||
static VARIABLE_MODEL = new Type<
|
||||
IVariableModelStatic<IVariableState> & IVariableModel<IVariableState>
|
||||
>('variableModel');
|
||||
|
||||
static VARIABLE_MAP = new Type<IVariableMap<IVariableModel<IVariableState>>>(
|
||||
'variableMap',
|
||||
|
||||
@@ -22,10 +22,7 @@ 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 {
|
||||
isAutoHideable,
|
||||
type IAutoHideable,
|
||||
} from '../interfaces/i_autohideable.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';
|
||||
@@ -1150,10 +1147,7 @@ export class Toolbox
|
||||
// If navigating to anything other than the toolbox's flyout then clear the
|
||||
// selection so that the toolbox's flyout can automatically close.
|
||||
if (!nextTree || nextTree !== this.flyout?.getWorkspace()) {
|
||||
this.clearSelection();
|
||||
if (this.flyout && isAutoHideable(this.flyout)) {
|
||||
this.flyout.autoHide(false);
|
||||
}
|
||||
this.autoHide(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,9 @@ export class VariableMap
|
||||
variable: IVariableModel<IVariableState>,
|
||||
newType: string,
|
||||
): IVariableModel<IVariableState> {
|
||||
const oldType = variable.getType();
|
||||
if (oldType === newType) return variable;
|
||||
|
||||
this.variableMap.get(variable.getType())?.delete(variable.getId());
|
||||
variable.setType(newType);
|
||||
const newTypeVariables =
|
||||
@@ -118,6 +121,13 @@ export class VariableMap
|
||||
if (!this.variableMap.has(newType)) {
|
||||
this.variableMap.set(newType, newTypeVariables);
|
||||
}
|
||||
eventUtils.fire(
|
||||
new (eventUtils.get(EventType.VAR_TYPE_CHANGE))(
|
||||
variable,
|
||||
oldType,
|
||||
newType,
|
||||
),
|
||||
);
|
||||
return variable;
|
||||
}
|
||||
|
||||
@@ -245,9 +255,9 @@ export class VariableMap
|
||||
}
|
||||
const id = opt_id || idGenerator.genUid();
|
||||
const type = opt_type || '';
|
||||
const VariableModel = registry.getObject(
|
||||
const VariableModel = registry.getClassFromOptions(
|
||||
registry.Type.VARIABLE_MODEL,
|
||||
registry.DEFAULT,
|
||||
this.workspace.options,
|
||||
true,
|
||||
);
|
||||
if (!VariableModel) {
|
||||
|
||||
@@ -469,7 +469,7 @@ export class Workspace {
|
||||
'Blockly.Workspace.getVariableUsesById',
|
||||
'v12',
|
||||
'v13',
|
||||
'Blockly.Workspace.getVariableMap().getVariableUsesById',
|
||||
'Blockly.Variables.getVariableUsesById',
|
||||
);
|
||||
return getVariableUsesById(this, id);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {Block} from './block.js';
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import type {BlocklyOptions} from './blockly_options.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import {TextInputBubble} from './bubbles/textinput_bubble.js';
|
||||
import {COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/collapse_comment_bar_button.js';
|
||||
import {COMMENT_EDITOR_FOCUS_IDENTIFIER} from './comments/comment_editor.js';
|
||||
import {COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/delete_comment_bar_button.js';
|
||||
@@ -1673,7 +1674,10 @@ export class WorkspaceSvg
|
||||
/** Clean up the workspace by ordering all the blocks in a column such that none overlap. */
|
||||
cleanUp() {
|
||||
this.setResizesEnabled(false);
|
||||
eventUtils.setGroup(true);
|
||||
const existingGroup = eventUtils.getGroup();
|
||||
if (!existingGroup) {
|
||||
eventUtils.setGroup(true);
|
||||
}
|
||||
|
||||
const topBlocks = this.getTopBlocks(true);
|
||||
const movableBlocks = topBlocks.filter((block) => block.isMovable());
|
||||
@@ -1721,7 +1725,7 @@ export class WorkspaceSvg
|
||||
block.getHeightWidth().height +
|
||||
minBlockHeight;
|
||||
}
|
||||
eventUtils.setGroup(false);
|
||||
eventUtils.setGroup(existingGroup);
|
||||
this.setResizesEnabled(true);
|
||||
}
|
||||
|
||||
@@ -2726,6 +2730,19 @@ export class WorkspaceSvg
|
||||
previousNode: IFocusableNode | null,
|
||||
): IFocusableNode | null {
|
||||
if (!previousNode) {
|
||||
const flyout = this.targetWorkspace?.getFlyout();
|
||||
if (this.isFlyout && flyout) {
|
||||
// Return the first focusable item of the flyout.
|
||||
return (
|
||||
flyout
|
||||
.getContents()
|
||||
.find((flyoutItem) => {
|
||||
const element = flyoutItem.getElement();
|
||||
return isFocusableNode(element) && element.canBeFocused();
|
||||
})
|
||||
?.getElement() ?? null
|
||||
);
|
||||
}
|
||||
return this.getTopBlocks(true)[0] ?? null;
|
||||
} else return null;
|
||||
}
|
||||
@@ -2868,6 +2885,11 @@ export class WorkspaceSvg
|
||||
bubble.getFocusableElement().id === id
|
||||
) {
|
||||
return bubble;
|
||||
} else if (
|
||||
bubble instanceof TextInputBubble &&
|
||||
bubble.getEditor().getFocusableElement().id === id
|
||||
) {
|
||||
return bubble.getEditor();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2889,11 +2911,9 @@ export class WorkspaceSvg
|
||||
// Only hide the flyout if the flyout's workspace is losing focus and that
|
||||
// focus isn't returning to the flyout itself, the toolbox, or ephemeral.
|
||||
if (getFocusManager().ephemeralFocusTaken()) return;
|
||||
const flyout = this.targetWorkspace.getFlyout();
|
||||
const toolbox = this.targetWorkspace.getToolbox();
|
||||
if (toolbox && nextTree === toolbox) return;
|
||||
if (toolbox) toolbox.clearSelection();
|
||||
if (flyout && isAutoHideable(flyout)) flyout.autoHide(false);
|
||||
if (isAutoHideable(toolbox)) toolbox.autoHide(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
463
package-lock.json
generated
463
package-lock.json
generated
@@ -13,24 +13,25 @@
|
||||
"jsdom": "26.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blockly/block-test": "^7.0.1",
|
||||
"@blockly/dev-tools": "^9.0.0",
|
||||
"@blockly/theme-modern": "^6.0.3",
|
||||
"@blockly/block-test": "^7.0.2",
|
||||
"@blockly/dev-tools": "^9.0.2",
|
||||
"@blockly/theme-modern": "^7.0.1",
|
||||
"@hyperjump/browser": "^1.1.4",
|
||||
"@hyperjump/json-schema": "^1.5.0",
|
||||
"@microsoft/api-documenter": "^7.22.4",
|
||||
"@microsoft/api-documenter": "7.22.4",
|
||||
"@microsoft/api-extractor": "^7.29.5",
|
||||
"ajv": "^8.17.1",
|
||||
"async-done": "^2.0.0",
|
||||
"chai": "^5.1.1",
|
||||
"concurrently": "^9.0.1",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-jsdoc": "^51.3.1",
|
||||
"eslint-plugin-jsdoc": "^52.0.2",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"glob": "^11.0.1",
|
||||
"globals": "^16.0.0",
|
||||
"google-closure-compiler": "^20250625.0.0",
|
||||
"google-closure-compiler": "^20250709.0.0",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-gzip": "^1.4.2",
|
||||
@@ -89,10 +90,11 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@blockly/block-test": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.1.tgz",
|
||||
"integrity": "sha512-w91ZZbpJDKGQJVO7gKqQaM17ffcsW1ktrnSTz/OpDw5R4H+1q05NgWO5gYzGPzLfFdvPcrkc0v00KhD4UG7BRA==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.2.tgz",
|
||||
"integrity": "sha512-fwbJnMiH4EoX/CR0ZTGzSKaGfpRBn4nudquoWfvG4ekkhTjaNTldDdHvUSeyexzvwZZcT6M4I1Jtq3IoomTKEg==",
|
||||
"dev": true,
|
||||
"license": "Apache 2.0",
|
||||
"engines": {
|
||||
"node": ">=8.17.0"
|
||||
},
|
||||
@@ -101,13 +103,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/dev-tools": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-9.0.1.tgz",
|
||||
"integrity": "sha512-OnY24Up00owts0VtOaokUmOQdzH+K1PNcr3LC3huwa9PO0TlKiXTq4V5OuIqBS++enyj93gXQ8PhvFGudkogTQ==",
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-9.0.2.tgz",
|
||||
"integrity": "sha512-Ic/+BkqEvLRZxzNQVW/FKXx1cB042xXXPTSmNlTv2qr4oY+hN2fwBtHj3PirBWAzWgMOF8VDTj/EXL36jH1/lg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@blockly/block-test": "^7.0.1",
|
||||
"@blockly/block-test": "^7.0.2",
|
||||
"@blockly/theme-dark": "^8.0.1",
|
||||
"@blockly/theme-deuteranopia": "^7.0.1",
|
||||
"@blockly/theme-highcontrast": "^7.0.1",
|
||||
@@ -235,15 +237,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/theme-modern": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-modern/-/theme-modern-6.0.10.tgz",
|
||||
"integrity": "sha512-xOVf5Vq5ACgbVsaNAKWb5cE0msUfBxj1G1asp0aBmWo1QCr3Yze4rUtFDaNIoeCd8EsRpuWZgBYg74zPL9eAow==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-modern/-/theme-modern-7.0.1.tgz",
|
||||
"integrity": "sha512-aMI3OBp8KCbLU1O14FLUlocK7IeMOyiSenlTJ4lwGcBmZntM2OIcx6o89oAIeq6HkmaH7vMlK+/AgqdB3k0y3A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"blockly": "^11.0.0"
|
||||
"blockly": "^12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/theme-tritanopia": {
|
||||
@@ -485,6 +488,23 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||
@@ -498,6 +518,13 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz",
|
||||
@@ -912,17 +939,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/api-documenter": {
|
||||
"version": "7.26.26",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.26.26.tgz",
|
||||
"integrity": "sha512-085FwdwQcXGvwtMJFajwhu5eZOQ3PXsyLIoq3WXAQr/7M6Vn59GMGjuB/+lIXqmWKkxzeFAX5f9sKqr9X7zI3g==",
|
||||
"version": "7.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.22.4.tgz",
|
||||
"integrity": "sha512-d4htEhBd8UkFKff/+/nAi/z7rrspm1DanFmsRHLUp4gKMo/8hYDH/IQBWB4r9X/8X72jCv3I++VVWAfichL1rw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/api-extractor-model": "7.30.6",
|
||||
"@microsoft/tsdoc": "~0.15.1",
|
||||
"@rushstack/node-core-library": "5.13.1",
|
||||
"@rushstack/terminal": "0.15.3",
|
||||
"@rushstack/ts-command-line": "5.0.1",
|
||||
"@microsoft/api-extractor-model": "7.26.8",
|
||||
"@microsoft/tsdoc": "0.14.2",
|
||||
"@rushstack/node-core-library": "3.58.0",
|
||||
"@rushstack/ts-command-line": "4.13.2",
|
||||
"colors": "~1.2.1",
|
||||
"js-yaml": "~3.13.1",
|
||||
"resolve": "~1.22.1"
|
||||
},
|
||||
@@ -930,6 +957,106 @@
|
||||
"api-documenter": "bin/api-documenter"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/api-documenter/node_modules/@microsoft/api-extractor-model": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.26.8.tgz",
|
||||
"integrity": "sha512-ESj3bBJkiMg/8tS0PW4+2rUgTVwOEfy41idTnFgdbVX+O50bN6S99MV6FIPlCZWCnRDcBfwxRXLdAkOQQ0JqGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/tsdoc": "0.14.2",
|
||||
"@microsoft/tsdoc-config": "~0.16.1",
|
||||
"@rushstack/node-core-library": "3.58.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/api-documenter/node_modules/@microsoft/tsdoc": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz",
|
||||
"integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@microsoft/api-documenter/node_modules/@microsoft/tsdoc-config": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz",
|
||||
"integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/tsdoc": "0.14.2",
|
||||
"ajv": "~6.12.6",
|
||||
"jju": "~1.4.0",
|
||||
"resolve": "~1.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/api-documenter/node_modules/@microsoft/tsdoc-config/node_modules/resolve": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
|
||||
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.1.0",
|
||||
"path-parse": "^1.0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/api-documenter/node_modules/@rushstack/node-core-library": {
|
||||
"version": "3.58.0",
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.58.0.tgz",
|
||||
"integrity": "sha512-DHAZ3LTOEq2/EGURznpTJDnB3SNE2CKMDXuviQ6afhru6RykE3QoqXkeyjbpLb5ib5cpIRCPE/wykNe0xmQj3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colors": "~1.2.1",
|
||||
"fs-extra": "~7.0.1",
|
||||
"import-lazy": "~4.0.0",
|
||||
"jju": "~1.4.0",
|
||||
"resolve": "~1.22.1",
|
||||
"semver": "~7.3.0",
|
||||
"z-schema": "~5.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/api-documenter/node_modules/@rushstack/ts-command-line": {
|
||||
"version": "4.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.13.2.tgz",
|
||||
"integrity": "sha512-bCU8qoL9HyWiciltfzg7GqdfODUeda/JpI0602kbN5YH22rzTxyqYvv7aRLENCM7XCQ1VRs7nMkEqgJUOU8Sag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/argparse": "1.0.38",
|
||||
"argparse": "~1.0.9",
|
||||
"colors": "~1.2.1",
|
||||
"string-argv": "~0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/api-documenter/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/api-documenter/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
@@ -952,6 +1079,29 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/api-documenter/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@microsoft/api-documenter/node_modules/semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/api-extractor": {
|
||||
"version": "7.52.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.8.tgz",
|
||||
@@ -1056,12 +1206,6 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1236,13 +1380,6 @@
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@rushstack/node-core-library/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rushstack/rig-package": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.3.tgz",
|
||||
@@ -1955,16 +2092,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2002,28 +2139,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/ajv": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz",
|
||||
"integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"uri-js": "^4.4.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ansi-gray": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz",
|
||||
@@ -2766,9 +2881,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
|
||||
"integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz",
|
||||
"integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2779,7 +2894,7 @@
|
||||
"pathval": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
@@ -2979,6 +3094,16 @@
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/colors": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz",
|
||||
"integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||
@@ -3114,10 +3239,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz",
|
||||
"integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==",
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz",
|
||||
"integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -3964,10 +4090,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsdoc": {
|
||||
"version": "51.3.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-51.3.1.tgz",
|
||||
"integrity": "sha512-9v/e6XyrLf1HIs/uPCgm3GcUpH4BeuGVZJk7oauKKyS7su7d5Q6zx4Fq6TiYh+w7+b4Svy7ZWVCcNZJNx3y52w==",
|
||||
"version": "52.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-52.0.2.tgz",
|
||||
"integrity": "sha512-fYrnc7OpRifxxKjH78Y9/D/EouQDYD3G++bpR1Y+A+fy+CMzKZAdGIiHTIxCd2U10hb2y1NxN5TJt9aupq1vmw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@es-joy/jsdoccomment": "~0.52.0",
|
||||
"are-docs-informative": "^0.0.2",
|
||||
@@ -4068,6 +4195,23 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
@@ -4092,6 +4236,13 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||
@@ -4340,6 +4491,23 @@
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
|
||||
@@ -4598,6 +4766,41 @@
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra/node_modules/jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra/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,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-mkdirp-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz",
|
||||
@@ -4949,13 +5152,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/google-closure-compiler": {
|
||||
"version": "20250625.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20250625.0.0.tgz",
|
||||
"integrity": "sha512-FQ6yKCRYwo4493Rq6lZrxpmWuJGZuuSruCdtArptkoThadzw4TM0YvQJvwRYnQDUpjj6/x7G14l2n/+8G39AIA==",
|
||||
"version": "20250709.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20250709.0.0.tgz",
|
||||
"integrity": "sha512-FUdjG7vri7Pi/iswJj1bFcE3cYOcGLnez2nKaEK8qSailRFQlnp8j9vuT60EOU8FLzckEPI0Sf882Q7vJPilFg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"chalk": "5.x",
|
||||
"google-closure-compiler-java": "^20250625.0.0",
|
||||
"google-closure-compiler-java": "^20250709.0.0",
|
||||
"minimist": "1.x",
|
||||
"vinyl": "3.x",
|
||||
"vinyl-sourcemaps-apply": "^0.2.0"
|
||||
@@ -4967,67 +5171,72 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"google-closure-compiler-linux": "^20250625.0.0",
|
||||
"google-closure-compiler-linux-arm64": "^20250625.0.0",
|
||||
"google-closure-compiler-macos": "^20250625.0.0",
|
||||
"google-closure-compiler-windows": "^20250625.0.0"
|
||||
"google-closure-compiler-linux": "^20250709.0.0",
|
||||
"google-closure-compiler-linux-arm64": "^20250709.0.0",
|
||||
"google-closure-compiler-macos": "^20250709.0.0",
|
||||
"google-closure-compiler-windows": "^20250709.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/google-closure-compiler-java": {
|
||||
"version": "20250625.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-compiler-java/-/google-closure-compiler-java-20250625.0.0.tgz",
|
||||
"integrity": "sha512-T916Kvb7JYaIiH9spiJXVKeualLV7PO/KXOJzMhLrW4M6etfvr3s2cTqlhUk+BrxvgxqWBWFbMDRUZbVGPnBaw==",
|
||||
"dev": true
|
||||
"version": "20250709.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-compiler-java/-/google-closure-compiler-java-20250709.0.0.tgz",
|
||||
"integrity": "sha512-gyriPJ8nYxYVa5wqeMJZsOdFoDDcHSmGHG9VNYjQrcdIOWyxW9Ggcb2gtrI/MEa54CLoRbzUJ12ELO1mzePMlQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/google-closure-compiler-linux": {
|
||||
"version": "20250625.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-compiler-linux/-/google-closure-compiler-linux-20250625.0.0.tgz",
|
||||
"integrity": "sha512-2cOYLfG7RF49FnGG+yBGlEndE0es8D7+YIGgF8KnGIkxrfiZhOTyQftFx4z48TZ1Be/1JtM2eNXbD2fuR9nJdA==",
|
||||
"version": "20250709.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-compiler-linux/-/google-closure-compiler-linux-20250709.0.0.tgz",
|
||||
"integrity": "sha512-kpl9W+696vnGzpa/ewfwpsRR3t42g3CDQ5hFjQAitxtZpnejU7ik94+O8D+56049zS2O85LdWRDCbckvzEXw+w==",
|
||||
"cpu": [
|
||||
"x32",
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/google-closure-compiler-linux-arm64": {
|
||||
"version": "20250625.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-compiler-linux-arm64/-/google-closure-compiler-linux-arm64-20250625.0.0.tgz",
|
||||
"integrity": "sha512-2vKY8UpL03CFe+k1qFma/HnUZnTM3V3K5ukxmk/Xwt3D7CTwn/039zA3AjxsGW5vLp4guVyLtqbS711KeGpLNA==",
|
||||
"version": "20250709.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-compiler-linux-arm64/-/google-closure-compiler-linux-arm64-20250709.0.0.tgz",
|
||||
"integrity": "sha512-3mLAD9JpAM0StUb2VTOw4L/rIxksTO7lOfuI0+OyexQfLIRLM8M9jeUgrOAPbmgDsyYZ8Q3pHX2qcnURexZsrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/google-closure-compiler-macos": {
|
||||
"version": "20250625.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-compiler-macos/-/google-closure-compiler-macos-20250625.0.0.tgz",
|
||||
"integrity": "sha512-/S3d5/oKKw2pEu42Bn+fnoKR0cAjlhOQP1IM0D1aDqNS+jMUXo4bV7RSVB+NSVL65XxIVQOqbnkD5Cfoe8lbrw==",
|
||||
"version": "20250709.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-compiler-macos/-/google-closure-compiler-macos-20250709.0.0.tgz",
|
||||
"integrity": "sha512-2/MXSVgM+HmnzwbyWdfY2ZVjKgK8LFtCKhsQQhsSV/f2jnrHcuG9+RkzLrzQsO1zPpHaLcXAkizf4AUpCfuzBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/google-closure-compiler-windows": {
|
||||
"version": "20250625.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-compiler-windows/-/google-closure-compiler-windows-20250625.0.0.tgz",
|
||||
"integrity": "sha512-YBNRFTSuWXDJad1pJ1SPjPFpgImrQr7XeW1D9YrPCv1T5cfM8vy01jFkZIDuUha38kHsPvk7kG3rkYYrJpD8+Q==",
|
||||
"version": "20250709.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-compiler-windows/-/google-closure-compiler-windows-20250709.0.0.tgz",
|
||||
"integrity": "sha512-ZnmgRzx0qIVQu0zw7ZTJQz3tMFVhwzeODZfXRnYDLeNkJA7IBaWsNHTALA7pUcgPM+YDDr4ihQOexMc0u4s7LQ==",
|
||||
"cpu": [
|
||||
"x32",
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -5328,10 +5537,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/gulp-rename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.0.0.tgz",
|
||||
"integrity": "sha512-97Vba4KBzbYmR5VBs9mWmK+HwIf5mj+/zioxfZhOKeXtx5ZjBk57KFlePf5nxq9QsTtFl0ejnHE3zTC9MHXqyQ==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.1.0.tgz",
|
||||
"integrity": "sha512-dGuzuH8jQGqCMqC544IEPhs5+O2l+IkdoSZsgd4kY97M1CxQeI3qrmweQBIrxLBbjbe/8uEWK8HHcNBc3OCy4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@@ -6192,9 +6402,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -6480,6 +6690,14 @@
|
||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@@ -9201,6 +9419,16 @@
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.15",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
||||
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/value-or-function": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz",
|
||||
@@ -9752,6 +9980,27 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/z-schema": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
|
||||
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"validator": "^13.7.0"
|
||||
},
|
||||
"bin": {
|
||||
"z-schema": "bin/z-schema"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"commander": "^9.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
|
||||
|
||||
13
package.json
13
package.json
@@ -100,24 +100,25 @@
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@blockly/block-test": "^7.0.1",
|
||||
"@blockly/dev-tools": "^9.0.0",
|
||||
"@blockly/theme-modern": "^6.0.3",
|
||||
"@blockly/block-test": "^7.0.2",
|
||||
"@blockly/dev-tools": "^9.0.2",
|
||||
"@blockly/theme-modern": "^7.0.1",
|
||||
"@hyperjump/browser": "^1.1.4",
|
||||
"@hyperjump/json-schema": "^1.5.0",
|
||||
"@microsoft/api-documenter": "^7.22.4",
|
||||
"@microsoft/api-documenter": "7.22.4",
|
||||
"@microsoft/api-extractor": "^7.29.5",
|
||||
"ajv": "^8.17.1",
|
||||
"async-done": "^2.0.0",
|
||||
"chai": "^5.1.1",
|
||||
"concurrently": "^9.0.1",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-jsdoc": "^51.3.1",
|
||||
"eslint-plugin-jsdoc": "^52.0.2",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"glob": "^11.0.1",
|
||||
"globals": "^16.0.0",
|
||||
"google-closure-compiler": "^20250625.0.0",
|
||||
"google-closure-compiler": "^20250709.0.0",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-gzip": "^1.4.2",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js b/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js
|
||||
index 0f4e2ba..3af2014 100644
|
||||
index 5284d10..4f8b439 100644
|
||||
--- a/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js
|
||||
+++ b/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js
|
||||
@@ -893,12 +893,15 @@ class MarkdownDocumenter {
|
||||
@@ -877,12 +877,14 @@ class MarkdownDocumenter {
|
||||
}
|
||||
_writeBreadcrumb(output, apiItem) {
|
||||
const configuration = this._tsdocConfiguration;
|
||||
@@ -19,28 +19,23 @@ index 0f4e2ba..3af2014 100644
|
||||
+ // linkText: 'Home',
|
||||
+ // urlDestination: this._getLinkFilenameForApiItem(this._apiModel)
|
||||
+ // }));
|
||||
+
|
||||
+ let first = true;
|
||||
for (const hierarchyItem of apiItem.getHierarchy()) {
|
||||
switch (hierarchyItem.kind) {
|
||||
case api_extractor_model_1.ApiItemKind.Model:
|
||||
@@ -908,18 +911,24 @@ class MarkdownDocumenter {
|
||||
@@ -892,18 +894,23 @@ class MarkdownDocumenter {
|
||||
// this may change in the future.
|
||||
break;
|
||||
default:
|
||||
- output.appendNodesInParagraph([
|
||||
- new tsdoc_1.DocPlainText({
|
||||
- configuration,
|
||||
- text: ' > '
|
||||
- }),
|
||||
+ if (!first) {
|
||||
+ // Only print the breadcrumb separator if it's not the first item we're printing.
|
||||
+ output.appendNodeInParagraph(
|
||||
+ new tsdoc_1.DocPlainText({
|
||||
+ configuration,
|
||||
+ text: ' > '
|
||||
+ })
|
||||
+ );
|
||||
new tsdoc_1.DocPlainText({
|
||||
configuration,
|
||||
text: ' > '
|
||||
- }),
|
||||
+ }));
|
||||
+ }
|
||||
+ first = false;
|
||||
+ output.appendNodeInParagraph(
|
||||
@@ -55,7 +50,7 @@ index 0f4e2ba..3af2014 100644
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -992,11 +1001,8 @@ class MarkdownDocumenter {
|
||||
@@ -968,11 +975,8 @@ class MarkdownDocumenter {
|
||||
// For overloaded methods, add a suffix such as "MyClass.myMethod_2".
|
||||
let qualifiedName = Utilities_1.Utilities.getSafeFilenameForName(hierarchyItem.displayName);
|
||||
if (api_extractor_model_1.ApiParameterListMixin.isBaseClassOf(hierarchyItem)) {
|
||||
@@ -69,7 +64,7 @@ index 0f4e2ba..3af2014 100644
|
||||
}
|
||||
switch (hierarchyItem.kind) {
|
||||
case api_extractor_model_1.ApiItemKind.Model:
|
||||
@@ -1007,7 +1013,8 @@ class MarkdownDocumenter {
|
||||
@@ -983,7 +987,8 @@ class MarkdownDocumenter {
|
||||
baseName = Utilities_1.Utilities.getSafeFilenameForName(node_core_library_1.PackageName.getUnscopedName(hierarchyItem.displayName));
|
||||
break;
|
||||
default:
|
||||
@@ -2,8 +2,8 @@ import {execSync} from 'child_process';
|
||||
import {Extractor} from 'markdown-tables-to-json';
|
||||
import * as fs from 'fs';
|
||||
import * as gulp from 'gulp';
|
||||
import * as header from 'gulp-header';
|
||||
import * as replace from 'gulp-replace';
|
||||
import header from 'gulp-header';
|
||||
import replace from 'gulp-replace';
|
||||
|
||||
const DOCS_DIR = 'docs';
|
||||
|
||||
|
||||
611
tests/browser/test/clipboard_test.mjs
Normal file
611
tests/browser/test/clipboard_test.mjs
Normal file
@@ -0,0 +1,611 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as chai from 'chai';
|
||||
import {Key} from 'webdriverio';
|
||||
import {
|
||||
PAUSE_TIME,
|
||||
clickWorkspace,
|
||||
focusOnBlock,
|
||||
getAllBlocks,
|
||||
getBlockTypeFromWorkspace,
|
||||
getCategory,
|
||||
getSelectedBlockId,
|
||||
getSelectedBlockType,
|
||||
openMutatorForBlock,
|
||||
testFileLocations,
|
||||
testSetup,
|
||||
} from './test_setup.mjs';
|
||||
|
||||
const testBlockJson = {
|
||||
'blocks': {
|
||||
'languageVersion': 0,
|
||||
'blocks': [
|
||||
{
|
||||
'type': 'controls_repeat_ext',
|
||||
'id': 'controls_repeat_1',
|
||||
'x': 88,
|
||||
'y': 88,
|
||||
'inputs': {
|
||||
'TIMES': {
|
||||
'shadow': {
|
||||
'type': 'math_number',
|
||||
'id': 'math_number_shadow_1',
|
||||
'fields': {
|
||||
'NUM': 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
'DO': {
|
||||
'block': {
|
||||
'type': 'controls_if',
|
||||
'id': 'controls_if_1',
|
||||
'inputs': {
|
||||
'IF0': {
|
||||
'block': {
|
||||
'type': 'logic_boolean',
|
||||
'id': 'logic_boolean_1',
|
||||
'fields': {
|
||||
'BOOL': 'TRUE',
|
||||
},
|
||||
},
|
||||
},
|
||||
'DO0': {
|
||||
'block': {
|
||||
'type': 'text_print',
|
||||
'id': 'text_print_1',
|
||||
'inputs': {
|
||||
'TEXT': {
|
||||
'shadow': {
|
||||
'type': 'text',
|
||||
'id': 'text_shadow_1',
|
||||
'fields': {
|
||||
'TEXT': 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
async function loadStartBlocks(browser) {
|
||||
await browser.execute((stringifiedJson) => {
|
||||
// Hangs forever if the json isn't stringified ¯\_(ツ)_/¯
|
||||
const testBlockJson = JSON.parse(stringifiedJson);
|
||||
const workspace = Blockly.common.getMainWorkspace();
|
||||
Blockly.serialization.workspaces.load(testBlockJson, workspace);
|
||||
}, JSON.stringify(testBlockJson));
|
||||
await browser.pause(PAUSE_TIME);
|
||||
}
|
||||
|
||||
suite('Clipboard test', async function () {
|
||||
// Setting timeout to unlimited as these tests take longer time to run
|
||||
this.timeout(0);
|
||||
|
||||
// Clear the workspace and load start blocks
|
||||
setup(async function () {
|
||||
this.browser = await testSetup(testFileLocations.PLAYGROUND);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
});
|
||||
|
||||
test('Paste block to/from main workspace', async function () {
|
||||
await loadStartBlocks(this.browser);
|
||||
// Select and copy the "true" block
|
||||
await focusOnBlock(this.browser, 'logic_boolean_1');
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
await this.browser.keys([Key.Ctrl, 'c']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check how many blocks there are before pasting
|
||||
const allBlocksBeforePaste = await getAllBlocks(this.browser);
|
||||
|
||||
// Paste the block while still in the main workspace
|
||||
await this.browser.keys([Key.Ctrl, 'v']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check result
|
||||
const allBlocksAfterPaste = await getAllBlocks(this.browser);
|
||||
chai.assert.equal(
|
||||
allBlocksAfterPaste.length,
|
||||
allBlocksBeforePaste.length + 1,
|
||||
'Expected there to be one additional block after paste',
|
||||
);
|
||||
const focusedBlockId = await getSelectedBlockId(this.browser);
|
||||
chai.assert.notEqual(
|
||||
focusedBlockId,
|
||||
'logic_boolean_1',
|
||||
'Newly pasted block should be selected',
|
||||
);
|
||||
const focusedBlockType = await getSelectedBlockType(this.browser);
|
||||
chai.assert.equal(
|
||||
focusedBlockType,
|
||||
'logic_boolean',
|
||||
'Newly pasted block should be selected',
|
||||
);
|
||||
});
|
||||
|
||||
test('Copying a block also copies and pastes its children', async function () {
|
||||
await loadStartBlocks(this.browser);
|
||||
// Select and copy the "if/else" block which has children
|
||||
await focusOnBlock(this.browser, 'controls_if_1');
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
await this.browser.keys([Key.Ctrl, 'c']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check how many blocks there are before pasting
|
||||
const allBlocksBeforePaste = await getAllBlocks(this.browser);
|
||||
|
||||
// Paste the block while still in the main workspace
|
||||
await this.browser.keys([Key.Ctrl, 'v']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check result
|
||||
const allBlocksAfterPaste = await getAllBlocks(this.browser);
|
||||
chai.assert.equal(
|
||||
allBlocksAfterPaste.length,
|
||||
allBlocksBeforePaste.length + 4,
|
||||
'Expected there to be four additional blocks after paste',
|
||||
);
|
||||
});
|
||||
|
||||
test('Paste shadow block to/from main workspace', async function () {
|
||||
await loadStartBlocks(this.browser);
|
||||
// Select and copy the shadow number block
|
||||
await focusOnBlock(this.browser, 'math_number_shadow_1');
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
await this.browser.keys([Key.Ctrl, 'c']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check how many blocks there are before pasting
|
||||
const allBlocksBeforePaste = await getAllBlocks(this.browser);
|
||||
|
||||
// Paste the block while still in the main workspace
|
||||
await this.browser.keys([Key.Ctrl, 'v']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check result
|
||||
const allBlocksAfterPaste = await getAllBlocks(this.browser);
|
||||
chai.assert.equal(
|
||||
allBlocksAfterPaste.length,
|
||||
allBlocksBeforePaste.length + 1,
|
||||
'Expected there to be one additional block after paste',
|
||||
);
|
||||
const focusedBlockId = await getSelectedBlockId(this.browser);
|
||||
chai.assert.notEqual(
|
||||
focusedBlockId,
|
||||
'math_number_shadow_1',
|
||||
'Newly pasted block should be selected',
|
||||
);
|
||||
const focusedBlockType = await getSelectedBlockType(this.browser);
|
||||
chai.assert.equal(
|
||||
focusedBlockType,
|
||||
'math_number',
|
||||
'Newly pasted block should be selected',
|
||||
);
|
||||
const focusedBlockIsShadow = await this.browser.execute(() => {
|
||||
return Blockly.common.getSelected().isShadow();
|
||||
});
|
||||
chai.assert.isFalse(
|
||||
focusedBlockIsShadow,
|
||||
'Expected the pasted version of the block to not be a shadow block',
|
||||
);
|
||||
});
|
||||
|
||||
test('Copy block from flyout, paste to main workspace', async function () {
|
||||
// Open flyout
|
||||
await getCategory(this.browser, 'Logic').then((category) =>
|
||||
category.click(),
|
||||
);
|
||||
|
||||
// Focus on first block in flyout
|
||||
await this.browser.execute(() => {
|
||||
const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace();
|
||||
const block = ws.getBlocksByType('controls_if')[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
});
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Copy
|
||||
await this.browser.keys([Key.Ctrl, 'c']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Select the main workspace
|
||||
await clickWorkspace(this.browser);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Paste
|
||||
await this.browser.keys([Key.Ctrl, 'v']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check that the block is now on the workspace and selected
|
||||
const allBlocks = await getAllBlocks(this.browser);
|
||||
chai.assert.equal(
|
||||
allBlocks.length,
|
||||
1,
|
||||
'Expected there to be one block on main workspace after paste from flyout',
|
||||
);
|
||||
|
||||
const focusedBlockType = await getSelectedBlockType(this.browser);
|
||||
chai.assert.equal(
|
||||
focusedBlockType,
|
||||
'controls_if',
|
||||
'Newly pasted block should be selected',
|
||||
);
|
||||
});
|
||||
|
||||
test('Copy block from flyout, paste while flyout focused', async function () {
|
||||
// Open flyout
|
||||
await getCategory(this.browser, 'Logic').then((category) =>
|
||||
category.click(),
|
||||
);
|
||||
|
||||
// Focus on first block in flyout
|
||||
await this.browser.execute(() => {
|
||||
const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace();
|
||||
const block = ws.getBlocksByType('controls_if')[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
});
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Copy
|
||||
await this.browser.keys([Key.Ctrl, 'c']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Paste
|
||||
await this.browser.keys([Key.Ctrl, 'v']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check that the flyout is closed
|
||||
const flyoutIsVisible = await this.browser
|
||||
.$('.blocklyToolboxFlyout')
|
||||
.then((elem) => elem.isDisplayed());
|
||||
chai.assert.isFalse(flyoutIsVisible, 'Expected flyout to not be open');
|
||||
|
||||
// Check that the block is now on the main workspace and selected
|
||||
const allBlocks = await getAllBlocks(this.browser);
|
||||
chai.assert.equal(
|
||||
allBlocks.length,
|
||||
1,
|
||||
'Expected there to be one block on main workspace after paste from flyout',
|
||||
);
|
||||
|
||||
const focusedBlockType = await getSelectedBlockType(this.browser);
|
||||
chai.assert.equal(
|
||||
focusedBlockType,
|
||||
'controls_if',
|
||||
'Newly pasted block should be selected',
|
||||
);
|
||||
});
|
||||
|
||||
test('Copy block from mutator flyout, paste to mutator workspace', async function () {
|
||||
// Load the start blocks
|
||||
await loadStartBlocks(this.browser);
|
||||
|
||||
// Open the controls_if mutator
|
||||
const block = await getBlockTypeFromWorkspace(
|
||||
this.browser,
|
||||
'controls_if',
|
||||
0,
|
||||
);
|
||||
await openMutatorForBlock(this.browser, block);
|
||||
|
||||
// Select the first block in the mutator flyout
|
||||
await this.browser.execute(
|
||||
(blockId, mutatorBlockType) => {
|
||||
const flyoutBlock = Blockly.getMainWorkspace()
|
||||
.getBlockById(blockId)
|
||||
.mutator.getWorkspace()
|
||||
.getFlyout()
|
||||
.getWorkspace()
|
||||
.getBlocksByType(mutatorBlockType)[0];
|
||||
|
||||
Blockly.getFocusManager().focusNode(flyoutBlock);
|
||||
},
|
||||
'controls_if_1',
|
||||
'controls_if_elseif',
|
||||
);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Copy
|
||||
await this.browser.keys([Key.Ctrl, 'c']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Paste
|
||||
await this.browser.keys([Key.Ctrl, 'v']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check that the block is now in the mutator workspace and selected
|
||||
const numberOfIfElseBlocks = await this.browser.execute(
|
||||
(blockId, mutatorBlockType) => {
|
||||
return Blockly.getMainWorkspace()
|
||||
.getBlockById(blockId)
|
||||
.mutator.getWorkspace()
|
||||
.getBlocksByType(mutatorBlockType).length;
|
||||
},
|
||||
'controls_if_1',
|
||||
'controls_if_elseif',
|
||||
);
|
||||
|
||||
chai.assert.equal(
|
||||
numberOfIfElseBlocks,
|
||||
1,
|
||||
'Expected there to be one if_else block in mutator workspace',
|
||||
);
|
||||
|
||||
const focusedBlockType = await getSelectedBlockType(this.browser);
|
||||
chai.assert.equal(
|
||||
focusedBlockType,
|
||||
'controls_if_elseif',
|
||||
'Newly pasted block should be selected',
|
||||
);
|
||||
});
|
||||
|
||||
test('Copy block from mutator flyout, paste to main workspace while mutator open', async function () {
|
||||
// Load the start blocks
|
||||
await loadStartBlocks(this.browser);
|
||||
|
||||
// Open the controls_if mutator
|
||||
const block = await getBlockTypeFromWorkspace(
|
||||
this.browser,
|
||||
'controls_if',
|
||||
0,
|
||||
);
|
||||
await openMutatorForBlock(this.browser, block);
|
||||
|
||||
// Select the first block in the mutator flyout
|
||||
await this.browser.execute(
|
||||
(blockId, mutatorBlockType) => {
|
||||
const flyoutBlock = Blockly.getMainWorkspace()
|
||||
.getBlockById(blockId)
|
||||
.mutator.getWorkspace()
|
||||
.getFlyout()
|
||||
.getWorkspace()
|
||||
.getBlocksByType(mutatorBlockType)[0];
|
||||
|
||||
Blockly.getFocusManager().focusNode(flyoutBlock);
|
||||
},
|
||||
'controls_if_1',
|
||||
'controls_if_elseif',
|
||||
);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Copy
|
||||
await this.browser.keys([Key.Ctrl, 'c']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Click the main workspace
|
||||
await clickWorkspace(this.browser);
|
||||
|
||||
// Paste
|
||||
await this.browser.keys([Key.Ctrl, 'v']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check that the block is now in the mutator workspace and selected
|
||||
const numberOfIfElseBlocks = await this.browser.execute(
|
||||
(blockId, mutatorBlockType) => {
|
||||
return Blockly.getMainWorkspace()
|
||||
.getBlockById(blockId)
|
||||
.mutator.getWorkspace()
|
||||
.getBlocksByType(mutatorBlockType).length;
|
||||
},
|
||||
'controls_if_1',
|
||||
'controls_if_elseif',
|
||||
);
|
||||
|
||||
chai.assert.equal(
|
||||
numberOfIfElseBlocks,
|
||||
1,
|
||||
'Expected there to be one if_else block in mutator workspace',
|
||||
);
|
||||
|
||||
const focusedBlockType = await getSelectedBlockType(this.browser);
|
||||
chai.assert.equal(
|
||||
focusedBlockType,
|
||||
'controls_if_elseif',
|
||||
'Newly pasted block should be selected',
|
||||
);
|
||||
|
||||
// Check that there are no new blocks on the main workspace
|
||||
const numberOfIfElseBlocksOnMainWorkspace = await this.browser.execute(
|
||||
(mutatorBlockType) => {
|
||||
return Blockly.getMainWorkspace().getBlocksByType(mutatorBlockType)
|
||||
.length;
|
||||
},
|
||||
'controls_if_elseif',
|
||||
);
|
||||
chai.assert.equal(
|
||||
numberOfIfElseBlocksOnMainWorkspace,
|
||||
0,
|
||||
'Mutator blocks should not appear on main workspace',
|
||||
);
|
||||
});
|
||||
|
||||
test('Copy block from mutator flyout, paste to main workspace while mutator closed', async function () {
|
||||
// Load the start blocks
|
||||
await loadStartBlocks(this.browser);
|
||||
|
||||
// Open the controls_if mutator
|
||||
const block = await getBlockTypeFromWorkspace(
|
||||
this.browser,
|
||||
'controls_if',
|
||||
0,
|
||||
);
|
||||
await openMutatorForBlock(this.browser, block);
|
||||
|
||||
// Select the first block in the mutator flyout
|
||||
await this.browser.execute(
|
||||
(blockId, mutatorBlockType) => {
|
||||
const flyoutBlock = Blockly.getMainWorkspace()
|
||||
.getBlockById(blockId)
|
||||
.mutator.getWorkspace()
|
||||
.getFlyout()
|
||||
.getWorkspace()
|
||||
.getBlocksByType(mutatorBlockType)[0];
|
||||
|
||||
Blockly.getFocusManager().focusNode(flyoutBlock);
|
||||
},
|
||||
'controls_if_1',
|
||||
'controls_if_elseif',
|
||||
);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Copy
|
||||
await this.browser.keys([Key.Ctrl, 'c']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Close the mutator flyout (calling this method on open mutator closes it)
|
||||
await openMutatorForBlock(this.browser, block);
|
||||
|
||||
// Click the main workspace
|
||||
await clickWorkspace(this.browser);
|
||||
|
||||
// Paste
|
||||
await this.browser.keys([Key.Ctrl, 'v']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check that there are no new blocks on the main workspace
|
||||
const numberOfIfElseBlocksOnMainWorkspace = await this.browser.execute(
|
||||
(mutatorBlockType) => {
|
||||
return Blockly.getMainWorkspace().getBlocksByType(mutatorBlockType)
|
||||
.length;
|
||||
},
|
||||
'controls_if_elseif',
|
||||
);
|
||||
chai.assert.equal(
|
||||
numberOfIfElseBlocksOnMainWorkspace,
|
||||
0,
|
||||
'Mutator blocks should not appear on main workspace',
|
||||
);
|
||||
});
|
||||
|
||||
test('Copy workspace comment, paste to main workspace', async function () {
|
||||
// Add a workspace comment to the workspace
|
||||
await this.browser.execute(() => {
|
||||
const workspace = Blockly.getMainWorkspace();
|
||||
const json = {
|
||||
'workspaceComments': [
|
||||
{
|
||||
'height': 100,
|
||||
'width': 120,
|
||||
'id': 'workspace_comment_1',
|
||||
'x': 13,
|
||||
'y': -12,
|
||||
'text': 'This is a comment',
|
||||
},
|
||||
],
|
||||
};
|
||||
Blockly.serialization.workspaces.load(json, workspace);
|
||||
});
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Select the workspace comment
|
||||
await this.browser.execute(() => {
|
||||
const comment = Blockly.getMainWorkspace().getCommentById(
|
||||
'workspace_comment_1',
|
||||
);
|
||||
Blockly.getFocusManager().focusNode(comment);
|
||||
});
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Copy
|
||||
await this.browser.keys([Key.Ctrl, 'c']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Click the main workspace
|
||||
await clickWorkspace(this.browser);
|
||||
|
||||
// Paste
|
||||
await this.browser.keys([Key.Ctrl, 'v']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check that there are 2 comments on the workspace
|
||||
const numberOfComments = await this.browser.execute(() => {
|
||||
return Blockly.getMainWorkspace().getTopComments().length;
|
||||
});
|
||||
chai.assert.equal(
|
||||
numberOfComments,
|
||||
2,
|
||||
'Expected 2 workspace comments after pasting',
|
||||
);
|
||||
});
|
||||
|
||||
test('Cut block from main workspace, paste to main workspace', async function () {
|
||||
await loadStartBlocks(this.browser);
|
||||
// Select and cut the "true" block
|
||||
await focusOnBlock(this.browser, 'logic_boolean_1');
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
await this.browser.keys([Key.Ctrl, 'x']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check that the "true" block was deleted
|
||||
const trueBlock = await this.browser.execute(() => {
|
||||
return Blockly.getMainWorkspace().getBlockById('logic_boolean_1') ?? null;
|
||||
});
|
||||
chai.assert.isNull(trueBlock);
|
||||
|
||||
// Check how many blocks there are before pasting
|
||||
const allBlocksBeforePaste = await getAllBlocks(this.browser);
|
||||
|
||||
// Paste the block while still in the main workspace
|
||||
await this.browser.keys([Key.Ctrl, 'v']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check result
|
||||
const allBlocksAfterPaste = await getAllBlocks(this.browser);
|
||||
chai.assert.equal(
|
||||
allBlocksAfterPaste.length,
|
||||
allBlocksBeforePaste.length + 1,
|
||||
'Expected there to be one additional block after paste',
|
||||
);
|
||||
});
|
||||
|
||||
test('Cannot cut block from flyout', async function () {
|
||||
// Open flyout
|
||||
await getCategory(this.browser, 'Logic').then((category) =>
|
||||
category.click(),
|
||||
);
|
||||
|
||||
// Focus on first block in flyout
|
||||
await this.browser.execute(() => {
|
||||
const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace();
|
||||
const block = ws.getBlocksByType('controls_if')[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
});
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Cut
|
||||
await this.browser.keys([Key.Ctrl, 'x']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Select the main workspace
|
||||
await clickWorkspace(this.browser);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Paste
|
||||
await this.browser.keys([Key.Ctrl, 'v']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
|
||||
// Check that no block was pasted
|
||||
const allBlocks = await getAllBlocks(this.browser);
|
||||
chai.assert.equal(
|
||||
allBlocks.length,
|
||||
0,
|
||||
'Expected no blocks in the workspace because nothing to paste',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -127,6 +127,23 @@ export const screenDirection = {
|
||||
LTR: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Focuses and selects a block with the provided ID.
|
||||
*
|
||||
* This throws an error if no block exists for the specified ID.
|
||||
*
|
||||
* @param browser The active WebdriverIO Browser object.
|
||||
* @param blockId The ID of the block to select.
|
||||
*/
|
||||
export async function focusOnBlock(browser, blockId) {
|
||||
return await browser.execute((blockId) => {
|
||||
const workspaceSvg = Blockly.getMainWorkspace();
|
||||
const block = workspaceSvg.getBlockById(blockId);
|
||||
if (!block) throw new Error(`No block found with ID: ${blockId}.`);
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
}, blockId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param browser The active WebdriverIO Browser object.
|
||||
* @return A Promise that resolves to the ID of the currently selected block.
|
||||
@@ -138,6 +155,17 @@ export async function getSelectedBlockId(browser) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param browser The active WebdriverIO Browser object.
|
||||
* @return A Promise that resolves to the ID of the currently selected block.
|
||||
*/
|
||||
export async function getSelectedBlockType(browser) {
|
||||
return await browser.execute(() => {
|
||||
// Note: selected is an ICopyable and I am assuming that it is a BlockSvg.
|
||||
return Blockly.common.getSelected()?.type;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param browser The active WebdriverIO Browser object.
|
||||
* @return A Promise that resolves to the selected block's root SVG element,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import * as chai from 'chai';
|
||||
import {Key} from 'webdriverio';
|
||||
import {
|
||||
dragBlockTypeFromFlyout,
|
||||
getBlockTypeFromCategory,
|
||||
getCategory,
|
||||
PAUSE_TIME,
|
||||
screenDirection,
|
||||
@@ -148,7 +148,12 @@ async function openCategories(browser, categoryList, directionMultiplier) {
|
||||
continue;
|
||||
}
|
||||
const blockType = await getNthBlockType(browser, categoryName, i);
|
||||
dragBlockTypeFromFlyout(browser, categoryName, blockType, 50, 20);
|
||||
const blockElem = await getBlockTypeFromCategory(
|
||||
browser,
|
||||
categoryName,
|
||||
blockType,
|
||||
);
|
||||
await blockElem.dragAndDrop({x: 50 * directionMultiplier, y: 20});
|
||||
await browser.pause(PAUSE_TIME);
|
||||
// Should be one top level block on the workspace.
|
||||
const topBlockCount = await browser.execute(() => {
|
||||
@@ -174,9 +179,9 @@ async function openCategories(browser, categoryList, directionMultiplier) {
|
||||
chai.assert.equal(failureCount, 0);
|
||||
}
|
||||
|
||||
// TODO (#9217) These take too long to run and are very flakey. Need to find a
|
||||
// better way to test whatever this is trying to test.
|
||||
suite.skip('Open toolbox categories', function () {
|
||||
// TODO (#9217) These take too long to run and are very flakey. Need to pull
|
||||
// these out into their own test runner.
|
||||
suite('Open toolbox categories', function () {
|
||||
this.timeout(0);
|
||||
|
||||
test('opening every toolbox category in the category toolbox in LTR', async function () {
|
||||
|
||||
@@ -201,6 +201,35 @@ suite('Blocks', function () {
|
||||
|
||||
assertUnpluggedHealFailed(blocks);
|
||||
});
|
||||
test('Disconnect top of stack with immovable sibling', function () {
|
||||
this.blocks.B.setMovable(false);
|
||||
this.blocks.A.unplug(true);
|
||||
assert.equal(this.blocks.A.nextConnection.targetBlock(), this.blocks.B);
|
||||
assert.isNull(this.blocks.B.nextConnection.targetBlock());
|
||||
assert.isNull(this.blocks.C.previousConnection.targetBlock());
|
||||
});
|
||||
test('Heal with immovable sibling mid-stack', function () {
|
||||
const blockD = this.workspace.newBlock('stack_block', 'd');
|
||||
this.blocks.C.nextConnection.connect(blockD.previousConnection);
|
||||
this.blocks.C.setMovable(false);
|
||||
this.blocks.B.unplug(true);
|
||||
assert.equal(this.blocks.A.nextConnection.targetBlock(), blockD);
|
||||
assert.equal(this.blocks.B.nextConnection.targetBlock(), this.blocks.C);
|
||||
assert.isNull(this.blocks.C.nextConnection.targetBlock());
|
||||
});
|
||||
test('Heal with immovable sibling and shadow sibling mid-stack', function () {
|
||||
const blockD = this.workspace.newBlock('stack_block', 'd');
|
||||
const blockE = this.workspace.newBlock('stack_block', 'e');
|
||||
this.blocks.C.nextConnection.connect(blockD.previousConnection);
|
||||
blockD.nextConnection.connect(blockE.previousConnection);
|
||||
this.blocks.C.setMovable(false);
|
||||
blockD.setShadow(true);
|
||||
this.blocks.B.unplug(true);
|
||||
assert.equal(this.blocks.A.nextConnection.targetBlock(), blockE);
|
||||
assert.equal(this.blocks.B.nextConnection.targetBlock(), this.blocks.C);
|
||||
assert.equal(this.blocks.C.nextConnection.targetBlock(), blockD);
|
||||
assert.isNull(blockD.nextConnection.targetBlock());
|
||||
});
|
||||
test('Child is shadow', function () {
|
||||
const blocks = this.blocks;
|
||||
blocks.C.setShadow(true);
|
||||
|
||||
@@ -61,6 +61,31 @@ suite('Clipboard', function () {
|
||||
);
|
||||
});
|
||||
|
||||
test('pasting blocks includes next blocks if requested', function () {
|
||||
const block = Blockly.serialization.blocks.append(
|
||||
{
|
||||
'type': 'controls_if',
|
||||
'id': 'blockId',
|
||||
'next': {
|
||||
'block': {
|
||||
'type': 'controls_if',
|
||||
'id': 'blockId2',
|
||||
},
|
||||
},
|
||||
},
|
||||
this.workspace,
|
||||
);
|
||||
assert.equal(this.workspace.getBlocksByType('controls_if').length, 2);
|
||||
// Both blocks should be copied
|
||||
const data = block.toCopyData(true);
|
||||
this.clock.runAll();
|
||||
|
||||
Blockly.clipboard.paste(data, this.workspace);
|
||||
this.clock.runAll();
|
||||
// After pasting, we should have gone from 2 to 4 blocks.
|
||||
assert.equal(this.workspace.getBlocksByType('controls_if').length, 4);
|
||||
});
|
||||
|
||||
test('copied from a mutator pastes them into the mutator', async function () {
|
||||
const block = Blockly.serialization.blocks.append(
|
||||
{
|
||||
|
||||
@@ -183,6 +183,57 @@ suite('Toolbox', function () {
|
||||
});
|
||||
});
|
||||
|
||||
suite('focus management', function () {
|
||||
setup(function () {
|
||||
this.toolbox = getInjectedToolbox();
|
||||
});
|
||||
teardown(function () {
|
||||
this.toolbox.dispose();
|
||||
});
|
||||
|
||||
test('Losing focus hides autoclosing flyout', function () {
|
||||
// Focus the toolbox and select a category to open the flyout.
|
||||
const target = this.toolbox.HtmlDiv.querySelector(
|
||||
'.blocklyToolboxCategory',
|
||||
);
|
||||
Blockly.getFocusManager().focusNode(this.toolbox);
|
||||
target.dispatchEvent(
|
||||
new PointerEvent('pointerdown', {
|
||||
target,
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
assert.isTrue(this.toolbox.getFlyout().isVisible());
|
||||
|
||||
// Focus the workspace to trigger the toolbox to close the flyout.
|
||||
Blockly.getFocusManager().focusNode(this.toolbox.getWorkspace());
|
||||
assert.isFalse(this.toolbox.getFlyout().isVisible());
|
||||
});
|
||||
|
||||
test('Losing focus does not hide non-autoclosing flyout', function () {
|
||||
// Make the toolbox's flyout non-autoclosing.
|
||||
this.toolbox.getFlyout().setAutoClose(false);
|
||||
|
||||
// Focus the toolbox and select a category to open the flyout.
|
||||
const target = this.toolbox.HtmlDiv.querySelector(
|
||||
'.blocklyToolboxCategory',
|
||||
);
|
||||
Blockly.getFocusManager().focusNode(this.toolbox);
|
||||
target.dispatchEvent(
|
||||
new PointerEvent('pointerdown', {
|
||||
target,
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
assert.isTrue(this.toolbox.getFlyout().isVisible());
|
||||
|
||||
// Focus the workspace; this should *not* trigger the toolbox to close the
|
||||
// flyout, which should remain visible.
|
||||
Blockly.getFocusManager().focusNode(this.toolbox.getWorkspace());
|
||||
assert.isTrue(this.toolbox.getFlyout().isVisible());
|
||||
});
|
||||
});
|
||||
|
||||
suite('onClick_', function () {
|
||||
setup(function () {
|
||||
this.toolbox = getInjectedToolbox();
|
||||
|
||||
@@ -505,5 +505,26 @@ suite('Variable Map', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('variable type change events', function () {
|
||||
test('are fired when a variable has its type changed', function () {
|
||||
const variable = this.variableMap.createVariable(
|
||||
'name1',
|
||||
'type1',
|
||||
'id1',
|
||||
);
|
||||
this.variableMap.changeVariableType(variable, 'type2');
|
||||
assertEventFired(
|
||||
this.eventSpy,
|
||||
Blockly.Events.VarTypeChange,
|
||||
{
|
||||
oldType: 'type1',
|
||||
newType: 'type2',
|
||||
varId: 'id1',
|
||||
},
|
||||
this.workspace.id,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user