mirror of
https://github.com/google/blockly.git
synced 2026-01-31 04:30:11 +01:00
release: merge develop into rc/v12.2.0
This commit is contained in:
66
.github/workflows/keyboard_plugin_test.yml
vendored
Normal file
66
.github/workflows/keyboard_plugin_test.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
# Workflow for running the keyboard navigation plugin's automated tests.
|
||||
|
||||
name: Keyboard Navigation Automated Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
webdriverio_tests:
|
||||
name: WebdriverIO tests
|
||||
timeout-minutes: 10
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout core Blockly
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: core-blockly
|
||||
|
||||
- name: Checkout keyboard navigation plugin
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'google/blockly-keyboard-experimentation'
|
||||
ref: 'main'
|
||||
path: blockly-keyboard-experimentation
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: NPM install
|
||||
run: |
|
||||
cd core-blockly
|
||||
npm install
|
||||
cd ..
|
||||
cd blockly-keyboard-experimentation
|
||||
npm install
|
||||
cd ..
|
||||
|
||||
- name: Link latest core develop with plugin
|
||||
run: |
|
||||
cd core-blockly
|
||||
npm run package
|
||||
cd dist
|
||||
npm link
|
||||
cd ../../blockly-keyboard-experimentation
|
||||
npm link blockly
|
||||
cd ..
|
||||
|
||||
- name: Run keyboard navigation plugin tests
|
||||
run: |
|
||||
cd blockly-keyboard-experimentation
|
||||
npm run test
|
||||
@@ -791,6 +791,7 @@ export class Block {
|
||||
isDeletable(): boolean {
|
||||
return (
|
||||
this.deletable &&
|
||||
!this.isInFlyout &&
|
||||
!this.shadow &&
|
||||
!this.isDeadOrDying() &&
|
||||
!this.workspace.isReadOnly()
|
||||
@@ -824,6 +825,7 @@ export class Block {
|
||||
isMovable(): boolean {
|
||||
return (
|
||||
this.movable &&
|
||||
!this.isInFlyout &&
|
||||
!this.shadow &&
|
||||
!this.isDeadOrDying() &&
|
||||
!this.workspace.isReadOnly()
|
||||
|
||||
@@ -1721,6 +1721,11 @@ export class BlockSvg
|
||||
this.dragStrategy = dragStrategy;
|
||||
}
|
||||
|
||||
/** Returns whether this block is copyable or not. */
|
||||
isCopyable(): boolean {
|
||||
return this.isOwnDeletable() && this.isOwnMovable();
|
||||
}
|
||||
|
||||
/** Returns whether this block is movable or not. */
|
||||
override isMovable(): boolean {
|
||||
return this.dragStrategy.isMovable();
|
||||
|
||||
@@ -153,7 +153,11 @@ export class MiniWorkspaceBubble extends Bubble {
|
||||
* are dealt with by resizing the workspace to show them.
|
||||
*/
|
||||
private bumpBlocksIntoBounds() {
|
||||
if (this.miniWorkspace.isDragging()) return;
|
||||
if (
|
||||
this.miniWorkspace.isDragging() &&
|
||||
!this.miniWorkspace.keyboardMoveInProgress
|
||||
)
|
||||
return;
|
||||
|
||||
const MARGIN = 20;
|
||||
|
||||
@@ -185,7 +189,15 @@ export class MiniWorkspaceBubble extends Bubble {
|
||||
* mini workspace.
|
||||
*/
|
||||
private updateBubbleSize() {
|
||||
if (this.miniWorkspace.isDragging()) return;
|
||||
if (
|
||||
this.miniWorkspace.isDragging() &&
|
||||
!this.miniWorkspace.keyboardMoveInProgress
|
||||
)
|
||||
return;
|
||||
|
||||
// Disable autolayout if a keyboard move is in progress to prevent the
|
||||
// mutator bubble from jumping around.
|
||||
this.autoLayout &&= !this.miniWorkspace.keyboardMoveInProgress;
|
||||
|
||||
const currSize = this.getSize();
|
||||
const newSize = this.calculateWorkspaceSize();
|
||||
|
||||
@@ -173,6 +173,11 @@ export class TextInputBubble extends Bubble {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export {CommentEditor} from './comments/comment_editor.js';
|
||||
export {CommentView} from './comments/comment_view.js';
|
||||
export {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
|
||||
export {WorkspaceComment} from './comments/workspace_comment.js';
|
||||
|
||||
190
core/comments/comment_editor.ts
Normal file
190
core/comments/comment_editor.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as browserEvents from '../browser_events.js';
|
||||
import {getFocusManager} from '../focus_manager.js';
|
||||
import {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import {IFocusableTree} from '../interfaces/i_focusable_tree.js';
|
||||
import * as touch from '../touch.js';
|
||||
import * as dom from '../utils/dom.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
|
||||
/**
|
||||
* String added to the ID of a workspace comment to identify
|
||||
* the focusable node for the comment editor.
|
||||
*/
|
||||
export const COMMENT_EDITOR_FOCUS_IDENTIFIER = '_comment_textarea_';
|
||||
|
||||
/** The part of a comment that can be typed into. */
|
||||
export class CommentEditor implements IFocusableNode {
|
||||
id?: string;
|
||||
/** The foreignObject containing the HTML text area. */
|
||||
private foreignObject: SVGForeignObjectElement;
|
||||
|
||||
/** The text area where the user can type. */
|
||||
private textArea: HTMLTextAreaElement;
|
||||
|
||||
/** Listeners for changes to text. */
|
||||
private textChangeListeners: Array<
|
||||
(oldText: string, newText: string) => void
|
||||
> = [];
|
||||
|
||||
/** The current text of the comment. Updates on text area change. */
|
||||
private text: string = '';
|
||||
|
||||
constructor(
|
||||
public workspace: WorkspaceSvg,
|
||||
commentId?: string,
|
||||
private onFinishEditing?: () => void,
|
||||
) {
|
||||
this.foreignObject = dom.createSvgElement(Svg.FOREIGNOBJECT, {
|
||||
'class': 'blocklyCommentForeignObject',
|
||||
});
|
||||
const body = document.createElementNS(dom.HTML_NS, 'body');
|
||||
body.setAttribute('xmlns', dom.HTML_NS);
|
||||
body.className = 'blocklyMinimalBody';
|
||||
this.textArea = document.createElementNS(
|
||||
dom.HTML_NS,
|
||||
'textarea',
|
||||
) as HTMLTextAreaElement;
|
||||
dom.addClass(this.textArea, 'blocklyCommentText');
|
||||
dom.addClass(this.textArea, 'blocklyTextarea');
|
||||
dom.addClass(this.textArea, 'blocklyText');
|
||||
body.appendChild(this.textArea);
|
||||
this.foreignObject.appendChild(body);
|
||||
|
||||
if (commentId) {
|
||||
this.id = commentId + COMMENT_EDITOR_FOCUS_IDENTIFIER;
|
||||
this.textArea.setAttribute('id', this.id);
|
||||
}
|
||||
|
||||
// Register browser event listeners for the user typing in the textarea.
|
||||
browserEvents.conditionalBind(
|
||||
this.textArea,
|
||||
'change',
|
||||
this,
|
||||
this.onTextChange,
|
||||
);
|
||||
|
||||
// Register listener for pointerdown to focus the textarea.
|
||||
browserEvents.conditionalBind(
|
||||
this.textArea,
|
||||
'pointerdown',
|
||||
this,
|
||||
(e: PointerEvent) => {
|
||||
// don't allow this event to bubble up
|
||||
// and steal focus away from the editor/comment.
|
||||
e.stopPropagation();
|
||||
getFocusManager().focusNode(this);
|
||||
touch.clearTouchIdentifier();
|
||||
},
|
||||
);
|
||||
|
||||
// Register listener for keydown events that would finish editing.
|
||||
browserEvents.conditionalBind(
|
||||
this.textArea,
|
||||
'keydown',
|
||||
this,
|
||||
this.handleKeyDown,
|
||||
);
|
||||
}
|
||||
|
||||
/** Gets the dom structure for this comment editor. */
|
||||
getDom(): SVGForeignObjectElement {
|
||||
return this.foreignObject;
|
||||
}
|
||||
|
||||
/** Gets the current text of the comment. */
|
||||
getText(): string {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
/** Sets the current text of the comment and fires change listeners. */
|
||||
setText(text: string) {
|
||||
this.textArea.value = text;
|
||||
this.onTextChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers listeners when the text of the comment changes, either
|
||||
* programmatically or manually by the user.
|
||||
*/
|
||||
private onTextChange() {
|
||||
const oldText = this.text;
|
||||
this.text = this.textArea.value;
|
||||
// Loop through listeners backwards in case they remove themselves.
|
||||
for (let i = this.textChangeListeners.length - 1; i >= 0; i--) {
|
||||
this.textChangeListeners[i](oldText, this.text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do something when the user indicates they've finished editing.
|
||||
*
|
||||
* @param e Keyboard event.
|
||||
*/
|
||||
private handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' || (e.key === 'Enter' && (e.ctrlKey || e.metaKey))) {
|
||||
if (this.onFinishEditing) this.onFinishEditing();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/** Registers a callback that listens for text changes. */
|
||||
addTextChangeListener(listener: (oldText: string, newText: string) => void) {
|
||||
this.textChangeListeners.push(listener);
|
||||
}
|
||||
|
||||
/** Removes the given listener from the list of text change listeners. */
|
||||
removeTextChangeListener(listener: () => void) {
|
||||
this.textChangeListeners.splice(
|
||||
this.textChangeListeners.indexOf(listener),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
/** Sets the placeholder text displayed for an empty comment. */
|
||||
setPlaceholderText(text: string) {
|
||||
this.textArea.placeholder = text;
|
||||
}
|
||||
|
||||
/** Sets whether the textarea is editable. If not, the textarea will be readonly. */
|
||||
setEditable(isEditable: boolean) {
|
||||
if (isEditable) {
|
||||
this.textArea.removeAttribute('readonly');
|
||||
} else {
|
||||
this.textArea.setAttribute('readonly', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
/** Update the size of the comment editor element. */
|
||||
updateSize(size: Size, topBarSize: Size) {
|
||||
this.foreignObject.setAttribute(
|
||||
'height',
|
||||
`${size.height - topBarSize.height}`,
|
||||
);
|
||||
this.foreignObject.setAttribute('width', `${size.width}`);
|
||||
this.foreignObject.setAttribute('y', `${topBarSize.height}`);
|
||||
if (this.workspace.RTL) {
|
||||
this.foreignObject.setAttribute('x', `${-size.width}`);
|
||||
}
|
||||
}
|
||||
|
||||
getFocusableElement(): HTMLElement | SVGElement {
|
||||
return this.textArea;
|
||||
}
|
||||
getFocusableTree(): IFocusableTree {
|
||||
return this.workspace;
|
||||
}
|
||||
onNodeFocus(): void {}
|
||||
onNodeBlur(): void {}
|
||||
canBeFocused(): boolean {
|
||||
if (this.id) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import * as browserEvents from '../browser_events.js';
|
||||
import * as css from '../css.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node';
|
||||
import {IRenderedElement} from '../interfaces/i_rendered_element.js';
|
||||
import * as layers from '../layers.js';
|
||||
import * as touch from '../touch.js';
|
||||
@@ -15,6 +16,7 @@ import * as drag from '../utils/drag.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {CommentEditor} from './comment_editor.js';
|
||||
|
||||
export class CommentView implements IRenderedElement {
|
||||
/** The root group element of the comment view. */
|
||||
@@ -46,11 +48,8 @@ export class CommentView implements IRenderedElement {
|
||||
/** The resize handle element. */
|
||||
private resizeHandle: SVGImageElement;
|
||||
|
||||
/** The foreignObject containing the HTML text area. */
|
||||
private foreignObject: SVGForeignObjectElement;
|
||||
|
||||
/** The text area where the user can type. */
|
||||
private textArea: HTMLTextAreaElement;
|
||||
/** The part of the comment view that contains the textarea to edit the comment. */
|
||||
private commentEditor: CommentEditor;
|
||||
|
||||
/** The current size of the comment in workspace units. */
|
||||
private size: Size;
|
||||
@@ -64,14 +63,6 @@ export class CommentView implements IRenderedElement {
|
||||
/** The current location of the comment in workspace coordinates. */
|
||||
private location: Coordinate = new Coordinate(0, 0);
|
||||
|
||||
/** The current text of the comment. Updates on text area change. */
|
||||
private text: string = '';
|
||||
|
||||
/** Listeners for changes to text. */
|
||||
private textChangeListeners: Array<
|
||||
(oldText: string, newText: string) => void
|
||||
> = [];
|
||||
|
||||
/** Listeners for changes to size. */
|
||||
private sizeChangeListeners: Array<(oldSize: Size, newSize: Size) => void> =
|
||||
[];
|
||||
@@ -106,7 +97,10 @@ export class CommentView implements IRenderedElement {
|
||||
/** The default size of newly created comments. */
|
||||
static defaultCommentSize = new Size(120, 100);
|
||||
|
||||
constructor(readonly workspace: WorkspaceSvg) {
|
||||
constructor(
|
||||
readonly workspace: WorkspaceSvg,
|
||||
private commentId?: string,
|
||||
) {
|
||||
this.svgRoot = dom.createSvgElement(Svg.G, {
|
||||
'class': 'blocklyComment blocklyEditable blocklyDraggable',
|
||||
});
|
||||
@@ -122,8 +116,7 @@ export class CommentView implements IRenderedElement {
|
||||
textPreviewNode: this.textPreviewNode,
|
||||
} = this.createTopBar(this.svgRoot, workspace));
|
||||
|
||||
({foreignObject: this.foreignObject, textArea: this.textArea} =
|
||||
this.createTextArea(this.svgRoot));
|
||||
this.commentEditor = this.createTextArea();
|
||||
|
||||
this.resizeHandle = this.createResizeHandle(this.svgRoot, workspace);
|
||||
|
||||
@@ -236,33 +229,32 @@ export class CommentView implements IRenderedElement {
|
||||
/**
|
||||
* Creates the text area where users can type. Registers event listeners.
|
||||
*/
|
||||
private createTextArea(svgRoot: SVGGElement): {
|
||||
foreignObject: SVGForeignObjectElement;
|
||||
textArea: HTMLTextAreaElement;
|
||||
} {
|
||||
const foreignObject = dom.createSvgElement(
|
||||
Svg.FOREIGNOBJECT,
|
||||
{
|
||||
'class': 'blocklyCommentForeignObject',
|
||||
},
|
||||
svgRoot,
|
||||
private createTextArea() {
|
||||
// When the user is done editing comment, focus the entire comment.
|
||||
const onFinishEditing = () => this.svgRoot.focus();
|
||||
const commentEditor = new CommentEditor(
|
||||
this.workspace,
|
||||
this.commentId,
|
||||
onFinishEditing,
|
||||
);
|
||||
const body = document.createElementNS(dom.HTML_NS, 'body');
|
||||
body.setAttribute('xmlns', dom.HTML_NS);
|
||||
body.className = 'blocklyMinimalBody';
|
||||
const textArea = document.createElementNS(
|
||||
dom.HTML_NS,
|
||||
'textarea',
|
||||
) as HTMLTextAreaElement;
|
||||
dom.addClass(textArea, 'blocklyCommentText');
|
||||
dom.addClass(textArea, 'blocklyTextarea');
|
||||
dom.addClass(textArea, 'blocklyText');
|
||||
body.appendChild(textArea);
|
||||
foreignObject.appendChild(body);
|
||||
|
||||
browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange);
|
||||
this.svgRoot.appendChild(commentEditor.getDom());
|
||||
|
||||
return {foreignObject, textArea};
|
||||
commentEditor.addTextChangeListener((oldText, newText) => {
|
||||
this.updateTextPreview(newText);
|
||||
// Update size in case our minimum size increased.
|
||||
this.setSize(this.size);
|
||||
});
|
||||
|
||||
return commentEditor;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns The FocusableNode representing the editor portion of this comment.
|
||||
*/
|
||||
getEditorFocusableNode(): IFocusableNode {
|
||||
return this.commentEditor;
|
||||
}
|
||||
|
||||
/** Creates the DOM elements for the comment resize handle. */
|
||||
@@ -324,7 +316,7 @@ export class CommentView implements IRenderedElement {
|
||||
|
||||
this.updateHighlightRect(size);
|
||||
this.updateTopBarSize(size);
|
||||
this.updateTextAreaSize(size, topBarSize);
|
||||
this.commentEditor.updateSize(size, topBarSize);
|
||||
this.updateDeleteIconPosition(size, topBarSize, deleteSize);
|
||||
this.updateFoldoutIconPosition(topBarSize, foldoutSize);
|
||||
this.updateTextPreviewSize(
|
||||
@@ -360,7 +352,7 @@ export class CommentView implements IRenderedElement {
|
||||
foldoutSize: Size,
|
||||
deleteSize: Size,
|
||||
): Size {
|
||||
this.updateTextPreview(this.textArea.value ?? '');
|
||||
this.updateTextPreview(this.commentEditor.getText() ?? '');
|
||||
const textPreviewWidth = dom.getTextWidth(this.textPreview);
|
||||
|
||||
const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize);
|
||||
@@ -408,19 +400,6 @@ export class CommentView implements IRenderedElement {
|
||||
this.topBarBackground.setAttribute('width', `${size.width}`);
|
||||
}
|
||||
|
||||
/** Updates the size of the text area elements to reflect the new size. */
|
||||
private updateTextAreaSize(size: Size, topBarSize: Size) {
|
||||
this.foreignObject.setAttribute(
|
||||
'height',
|
||||
`${size.height - topBarSize.height}`,
|
||||
);
|
||||
this.foreignObject.setAttribute('width', `${size.width}`);
|
||||
this.foreignObject.setAttribute('y', `${topBarSize.height}`);
|
||||
if (this.workspace.RTL) {
|
||||
this.foreignObject.setAttribute('x', `${-size.width}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the position of the delete icon elements to reflect the new size.
|
||||
*/
|
||||
@@ -652,12 +631,11 @@ export class CommentView implements IRenderedElement {
|
||||
if (this.editable) {
|
||||
dom.addClass(this.svgRoot, 'blocklyEditable');
|
||||
dom.removeClass(this.svgRoot, 'blocklyReadonly');
|
||||
this.textArea.removeAttribute('readonly');
|
||||
} else {
|
||||
dom.removeClass(this.svgRoot, 'blocklyEditable');
|
||||
dom.addClass(this.svgRoot, 'blocklyReadonly');
|
||||
this.textArea.setAttribute('readonly', 'true');
|
||||
}
|
||||
this.commentEditor.setEditable(editable);
|
||||
}
|
||||
|
||||
/** Returns the current location of the comment in workspace coordinates. */
|
||||
@@ -678,49 +656,29 @@ export class CommentView implements IRenderedElement {
|
||||
);
|
||||
}
|
||||
|
||||
/** Retursn the current text of the comment. */
|
||||
/** Returns the current text of the comment. */
|
||||
getText() {
|
||||
return this.text;
|
||||
return this.commentEditor.getText();
|
||||
}
|
||||
|
||||
/** Sets the current text of the comment. */
|
||||
setText(text: string) {
|
||||
this.textArea.value = text;
|
||||
this.onTextChange();
|
||||
this.commentEditor.setText(text);
|
||||
}
|
||||
|
||||
/** Sets the placeholder text displayed for an empty comment. */
|
||||
setPlaceholderText(text: string) {
|
||||
this.textArea.placeholder = text;
|
||||
this.commentEditor.setPlaceholderText(text);
|
||||
}
|
||||
|
||||
/** Registers a callback that listens for text changes. */
|
||||
/** Registers a callback that listens for text changes on the comment editor. */
|
||||
addTextChangeListener(listener: (oldText: string, newText: string) => void) {
|
||||
this.textChangeListeners.push(listener);
|
||||
this.commentEditor.addTextChangeListener(listener);
|
||||
}
|
||||
|
||||
/** Removes the given listener from the list of text change listeners. */
|
||||
/** Removes the given listener from the comment editor. */
|
||||
removeTextChangeListener(listener: () => void) {
|
||||
this.textChangeListeners.splice(
|
||||
this.textChangeListeners.indexOf(listener),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers listeners when the text of the comment changes, either
|
||||
* programmatically or manually by the user.
|
||||
*/
|
||||
private onTextChange() {
|
||||
const oldText = this.text;
|
||||
this.text = this.textArea.value;
|
||||
this.updateTextPreview(this.text);
|
||||
// Update size in case our minimum size increased.
|
||||
this.setSize(this.size);
|
||||
// Loop through listeners backwards in case they remove themselves.
|
||||
for (let i = this.textChangeListeners.length - 1; i >= 0; i--) {
|
||||
this.textChangeListeners[i](oldText, this.text);
|
||||
}
|
||||
this.commentEditor.removeTextChangeListener(listener);
|
||||
}
|
||||
|
||||
/** Updates the preview text element to reflect the given text. */
|
||||
@@ -884,6 +842,11 @@ css.register(`
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.blocklyCommentText.blocklyActiveFocus {
|
||||
border-color: #fc3;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.blocklySelected .blocklyCommentHighlight {
|
||||
stroke: #fc3;
|
||||
stroke-width: 3px;
|
||||
|
||||
@@ -47,7 +47,7 @@ export class RenderedWorkspaceComment
|
||||
IFocusableNode
|
||||
{
|
||||
/** The class encompassing the svg elements making up the workspace comment. */
|
||||
private view: CommentView;
|
||||
view: CommentView;
|
||||
|
||||
public readonly workspace: WorkspaceSvg;
|
||||
|
||||
@@ -59,7 +59,7 @@ export class RenderedWorkspaceComment
|
||||
|
||||
this.workspace = workspace;
|
||||
|
||||
this.view = new CommentView(workspace);
|
||||
this.view = new CommentView(workspace, this.id);
|
||||
// Set the size to the default size as defined in the superclass.
|
||||
this.view.setSize(this.getSize());
|
||||
this.view.setEditable(this.isEditable());
|
||||
@@ -224,13 +224,7 @@ export class RenderedWorkspaceComment
|
||||
private startGesture(e: PointerEvent) {
|
||||
const gesture = this.workspace.getGesture(e);
|
||||
if (gesture) {
|
||||
if (browserEvents.isTargetInput(e)) {
|
||||
// If the text area was the focus, don't allow this event to bubble up
|
||||
// and steal focus away from the editor/comment.
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
gesture.handleCommentStart(e, this);
|
||||
}
|
||||
gesture.handleCommentStart(e, this);
|
||||
getFocusManager().focusNode(this);
|
||||
}
|
||||
}
|
||||
@@ -244,6 +238,11 @@ export class RenderedWorkspaceComment
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns whether this comment is copyable or not */
|
||||
isCopyable(): boolean {
|
||||
return this.isOwnMovable() && this.isOwnDeletable();
|
||||
}
|
||||
|
||||
/** Returns whether this comment is movable or not. */
|
||||
isMovable(): boolean {
|
||||
return this.dragStrategy.isMovable();
|
||||
@@ -334,6 +333,13 @@ export class RenderedWorkspaceComment
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The FocusableNode representing the editor portion of this comment.
|
||||
*/
|
||||
getEditorFocusableNode(): IFocusableNode {
|
||||
return this.view.getEditorFocusableNode();
|
||||
}
|
||||
|
||||
/** See IFocusableNode.getFocusableElement. */
|
||||
getFocusableElement(): HTMLElement | SVGElement {
|
||||
return this.getSvgRoot();
|
||||
|
||||
@@ -165,7 +165,11 @@ export class WorkspaceComment {
|
||||
* workspace is read-only.
|
||||
*/
|
||||
isMovable() {
|
||||
return this.isOwnMovable() && !this.workspace.isReadOnly();
|
||||
return (
|
||||
this.isOwnMovable() &&
|
||||
!this.workspace.isReadOnly() &&
|
||||
!this.workspace.isFlyout
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,7 +193,8 @@ export class WorkspaceComment {
|
||||
return (
|
||||
this.isOwnDeletable() &&
|
||||
!this.isDeadOrDying() &&
|
||||
!this.workspace.isReadOnly()
|
||||
!this.workspace.isReadOnly() &&
|
||||
!this.workspace.isFlyout
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -320,21 +320,28 @@ export function defineBlocks(blocks: {[key: string]: BlockDefinition}) {
|
||||
* @param e Key down event.
|
||||
*/
|
||||
export function globalShortcutHandler(e: KeyboardEvent) {
|
||||
const mainWorkspace = getMainWorkspace() as WorkspaceSvg;
|
||||
if (!mainWorkspace) {
|
||||
return;
|
||||
// This would ideally just be a `focusedTree instanceof WorkspaceSvg`, but
|
||||
// importing `WorkspaceSvg` (as opposed to just its type) causes cycles.
|
||||
let workspace: WorkspaceSvg = getMainWorkspace() as WorkspaceSvg;
|
||||
const focusedTree = getFocusManager().getFocusedTree();
|
||||
for (const ws of getAllWorkspaces()) {
|
||||
if (focusedTree === (ws as WorkspaceSvg)) {
|
||||
workspace = ws as WorkspaceSvg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
browserEvents.isTargetInput(e) ||
|
||||
(mainWorkspace.rendered && !mainWorkspace.isVisible())
|
||||
!workspace ||
|
||||
(workspace.rendered && !workspace.isFlyout && !workspace.isVisible())
|
||||
) {
|
||||
// When focused on an HTML text input widget, don't trap any keys.
|
||||
// Ignore keypresses on rendered workspaces that have been explicitly
|
||||
// hidden.
|
||||
return;
|
||||
}
|
||||
ShortcutRegistry.registry.onKeyDown(mainWorkspace, e);
|
||||
ShortcutRegistry.registry.onKeyDown(workspace, e);
|
||||
}
|
||||
|
||||
export const TEST_ONLY = {defineBlocksWithJsonArrayInternal};
|
||||
|
||||
@@ -14,7 +14,6 @@ import {BlockChange} from '../events/events_block_change.js';
|
||||
import {isBlockChange, isBlockCreate} from '../events/predicates.js';
|
||||
import {EventType} from '../events/type.js';
|
||||
import * as eventUtils from '../events/utils.js';
|
||||
import type {IBubble} from '../interfaces/i_bubble.js';
|
||||
import type {IHasBubble} from '../interfaces/i_has_bubble.js';
|
||||
import * as renderManagement from '../render_management.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
@@ -205,7 +204,7 @@ export class MutatorIcon extends Icon implements IHasBubble {
|
||||
}
|
||||
|
||||
/** See IHasBubble.getBubble. */
|
||||
getBubble(): IBubble | null {
|
||||
getBubble(): MiniWorkspaceBubble | null {
|
||||
return this.miniWorkspaceBubble;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,5 +23,5 @@ export interface IAutoHideable extends IComponent {
|
||||
|
||||
/** Returns true if the given object is autohideable. */
|
||||
export function isAutoHideable(obj: any): obj is IAutoHideable {
|
||||
return obj.autoHide !== undefined;
|
||||
return obj && typeof obj.autoHide === 'function';
|
||||
}
|
||||
|
||||
@@ -31,17 +31,17 @@ export interface ICommentIcon extends IIcon, IHasBubble, ISerializable {
|
||||
}
|
||||
|
||||
/** Checks whether the given object is an ICommentIcon. */
|
||||
export function isCommentIcon(obj: object): obj is ICommentIcon {
|
||||
export function isCommentIcon(obj: any): obj is ICommentIcon {
|
||||
return (
|
||||
isIcon(obj) &&
|
||||
hasBubble(obj) &&
|
||||
isSerializable(obj) &&
|
||||
(obj as any)['setText'] !== undefined &&
|
||||
(obj as any)['getText'] !== undefined &&
|
||||
(obj as any)['setBubbleSize'] !== undefined &&
|
||||
(obj as any)['getBubbleSize'] !== undefined &&
|
||||
(obj as any)['setBubbleLocation'] !== undefined &&
|
||||
(obj as any)['getBubbleLocation'] !== undefined &&
|
||||
typeof (obj as any).setText === 'function' &&
|
||||
typeof (obj as any).getText === 'function' &&
|
||||
typeof (obj as any).setBubbleSize === 'function' &&
|
||||
typeof (obj as any).getBubbleSize === 'function' &&
|
||||
typeof (obj as any).setBubbleLocation === 'function' &&
|
||||
typeof (obj as any).getBubbleLocation === 'function' &&
|
||||
obj.getType() === IconType.COMMENT
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,14 @@ export interface ICopyable<T extends ICopyData> extends ISelectable {
|
||||
* @returns Copy metadata.
|
||||
*/
|
||||
toCopyData(): T | null;
|
||||
|
||||
/**
|
||||
* Whether this instance is currently copyable. The standard implementation
|
||||
* is to return true if isOwnDeletable and isOwnMovable return true.
|
||||
*
|
||||
* @returns True if it can currently be copied.
|
||||
*/
|
||||
isCopyable?(): boolean;
|
||||
}
|
||||
|
||||
export namespace ICopyable {
|
||||
@@ -25,7 +33,7 @@ export namespace ICopyable {
|
||||
|
||||
export type ICopyData = ICopyable.ICopyData;
|
||||
|
||||
/** @returns true if the given object is copyable. */
|
||||
/** @returns true if the given object is an ICopyable. */
|
||||
export function isCopyable(obj: any): obj is ICopyable<ICopyData> {
|
||||
return obj.toCopyData !== undefined;
|
||||
return obj && typeof obj.toCopyData === 'function';
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ export interface IDeletable {
|
||||
/** Returns whether the given object is an IDeletable. */
|
||||
export function isDeletable(obj: any): obj is IDeletable {
|
||||
return (
|
||||
obj['isDeletable'] !== undefined &&
|
||||
obj['dispose'] !== undefined &&
|
||||
obj['setDeleteStyle'] !== undefined
|
||||
obj &&
|
||||
typeof obj.isDeletable === 'function' &&
|
||||
typeof obj.dispose === 'function' &&
|
||||
typeof obj.setDeleteStyle === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,11 +62,12 @@ export interface IDragStrategy {
|
||||
/** Returns whether the given object is an IDraggable or not. */
|
||||
export function isDraggable(obj: any): obj is IDraggable {
|
||||
return (
|
||||
obj.getRelativeToSurfaceXY !== undefined &&
|
||||
obj.isMovable !== undefined &&
|
||||
obj.startDrag !== undefined &&
|
||||
obj.drag !== undefined &&
|
||||
obj.endDrag !== undefined &&
|
||||
obj.revertDrag !== undefined
|
||||
obj &&
|
||||
typeof obj.getRelativeToSurfaceXY === 'function' &&
|
||||
typeof obj.isMovable === 'function' &&
|
||||
typeof obj.startDrag === 'function' &&
|
||||
typeof obj.drag === 'function' &&
|
||||
typeof obj.endDrag === 'function' &&
|
||||
typeof obj.revertDrag === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,16 +102,16 @@ export interface IFocusableNode {
|
||||
* Determines whether the provided object fulfills the contract of
|
||||
* IFocusableNode.
|
||||
*
|
||||
* @param object The object to test.
|
||||
* @param obj The object to test.
|
||||
* @returns Whether the provided object can be used as an IFocusableNode.
|
||||
*/
|
||||
export function isFocusableNode(object: any | null): object is IFocusableNode {
|
||||
export function isFocusableNode(obj: any): obj is IFocusableNode {
|
||||
return (
|
||||
object &&
|
||||
'getFocusableElement' in object &&
|
||||
'getFocusableTree' in object &&
|
||||
'onNodeFocus' in object &&
|
||||
'onNodeBlur' in object &&
|
||||
'canBeFocused' in object
|
||||
obj &&
|
||||
typeof obj.getFocusableElement === 'function' &&
|
||||
typeof obj.getFocusableTree === 'function' &&
|
||||
typeof obj.onNodeFocus === 'function' &&
|
||||
typeof obj.onNodeBlur === 'function' &&
|
||||
typeof obj.canBeFocused === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,17 +128,17 @@ export interface IFocusableTree {
|
||||
* Determines whether the provided object fulfills the contract of
|
||||
* IFocusableTree.
|
||||
*
|
||||
* @param object The object to test.
|
||||
* @param obj The object to test.
|
||||
* @returns Whether the provided object can be used as an IFocusableTree.
|
||||
*/
|
||||
export function isFocusableTree(object: any | null): object is IFocusableTree {
|
||||
export function isFocusableTree(obj: any): obj is IFocusableTree {
|
||||
return (
|
||||
object &&
|
||||
'getRootFocusableNode' in object &&
|
||||
'getRestoredFocusableNode' in object &&
|
||||
'getNestedTrees' in object &&
|
||||
'lookUpFocusableNode' in object &&
|
||||
'onTreeFocus' in object &&
|
||||
'onTreeBlur' in object
|
||||
obj &&
|
||||
typeof obj.getRootFocusableNode === 'function' &&
|
||||
typeof obj.getRestoredFocusableNode === 'function' &&
|
||||
typeof obj.getNestedTrees === 'function' &&
|
||||
typeof obj.lookUpFocusableNode === 'function' &&
|
||||
typeof obj.onTreeFocus === 'function' &&
|
||||
typeof obj.onTreeBlur === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface IHasBubble {
|
||||
/** Type guard that checks whether the given object is a IHasBubble. */
|
||||
export function hasBubble(obj: any): obj is IHasBubble {
|
||||
return (
|
||||
obj.bubbleIsVisible !== undefined && obj.setBubbleVisible !== undefined
|
||||
typeof obj.bubbleIsVisible === 'function' &&
|
||||
typeof obj.setBubbleVisible === 'function' &&
|
||||
typeof obj.getBubble === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,19 +98,19 @@ export interface IIcon extends IFocusableNode {
|
||||
/** Type guard that checks whether the given object is an IIcon. */
|
||||
export function isIcon(obj: any): obj is IIcon {
|
||||
return (
|
||||
obj.getType !== undefined &&
|
||||
obj.initView !== undefined &&
|
||||
obj.dispose !== undefined &&
|
||||
obj.getWeight !== undefined &&
|
||||
obj.getSize !== undefined &&
|
||||
obj.applyColour !== undefined &&
|
||||
obj.hideForInsertionMarker !== undefined &&
|
||||
obj.updateEditable !== undefined &&
|
||||
obj.updateCollapsed !== undefined &&
|
||||
obj.isShownWhenCollapsed !== undefined &&
|
||||
obj.setOffsetInBlock !== undefined &&
|
||||
obj.onLocationChange !== undefined &&
|
||||
obj.onClick !== undefined &&
|
||||
isFocusableNode(obj)
|
||||
isFocusableNode(obj) &&
|
||||
typeof (obj as IIcon).getType === 'function' &&
|
||||
typeof (obj as IIcon).initView === 'function' &&
|
||||
typeof (obj as IIcon).dispose === 'function' &&
|
||||
typeof (obj as IIcon).getWeight === 'function' &&
|
||||
typeof (obj as IIcon).getSize === 'function' &&
|
||||
typeof (obj as IIcon).applyColour === 'function' &&
|
||||
typeof (obj as IIcon).hideForInsertionMarker === 'function' &&
|
||||
typeof (obj as IIcon).updateEditable === 'function' &&
|
||||
typeof (obj as IIcon).updateCollapsed === 'function' &&
|
||||
typeof (obj as IIcon).isShownWhenCollapsed === 'function' &&
|
||||
typeof (obj as IIcon).setOffsetInBlock === 'function' &&
|
||||
typeof (obj as IIcon).onLocationChange === 'function' &&
|
||||
typeof (obj as IIcon).onClick === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ export interface LegacyProcedureDefBlock {
|
||||
|
||||
/** @internal */
|
||||
export function isLegacyProcedureDefBlock(
|
||||
block: object,
|
||||
): block is LegacyProcedureDefBlock {
|
||||
return (block as any).getProcedureDef !== undefined;
|
||||
obj: any,
|
||||
): obj is LegacyProcedureDefBlock {
|
||||
return obj && typeof obj.getProcedureDef === 'function';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -41,10 +41,11 @@ export interface LegacyProcedureCallBlock {
|
||||
|
||||
/** @internal */
|
||||
export function isLegacyProcedureCallBlock(
|
||||
block: object,
|
||||
): block is LegacyProcedureCallBlock {
|
||||
obj: any,
|
||||
): obj is LegacyProcedureCallBlock {
|
||||
return (
|
||||
(block as any).getProcedureCall !== undefined &&
|
||||
(block as any).renameProcedure !== undefined
|
||||
obj &&
|
||||
typeof obj.getProcedureCall === 'function' &&
|
||||
typeof obj.renameProcedure === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,5 +20,9 @@ export interface IObservable {
|
||||
* @internal
|
||||
*/
|
||||
export function isObservable(obj: any): obj is IObservable {
|
||||
return obj.startPublishing !== undefined && obj.stopPublishing !== undefined;
|
||||
return (
|
||||
obj &&
|
||||
typeof obj.startPublishing === 'function' &&
|
||||
typeof obj.stopPublishing === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,5 +21,5 @@ export interface IPaster<U extends ICopyData, T extends ICopyable<U>> {
|
||||
export function isPaster(
|
||||
obj: any,
|
||||
): obj is IPaster<ICopyData, ICopyable<ICopyData>> {
|
||||
return obj.paste !== undefined;
|
||||
return obj && typeof obj.paste === 'function';
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ export interface IProcedureBlock {
|
||||
export function isProcedureBlock(
|
||||
block: Block | IProcedureBlock,
|
||||
): block is IProcedureBlock {
|
||||
block = block as IProcedureBlock;
|
||||
return (
|
||||
(block as IProcedureBlock).getProcedureModel !== undefined &&
|
||||
(block as IProcedureBlock).doProcedureUpdate !== undefined &&
|
||||
(block as IProcedureBlock).isProcedureDef !== undefined
|
||||
typeof block.getProcedureModel === 'function' &&
|
||||
typeof block.doProcedureUpdate === 'function' &&
|
||||
typeof block.isProcedureDef === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ export interface IRenderedElement {
|
||||
* @returns True if the given object is an IRenderedElement.
|
||||
*/
|
||||
export function isRenderedElement(obj: any): obj is IRenderedElement {
|
||||
return obj['getSvgRoot'] !== undefined;
|
||||
return obj && typeof obj.getSvgRoot === 'function';
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@ export interface ISelectable extends IFocusableNode {
|
||||
}
|
||||
|
||||
/** Checks whether the given object is an ISelectable. */
|
||||
export function isSelectable(obj: object): obj is ISelectable {
|
||||
export function isSelectable(obj: any): obj is ISelectable {
|
||||
return (
|
||||
typeof (obj as any).id === 'string' &&
|
||||
(obj as any).workspace !== undefined &&
|
||||
(obj as any).select !== undefined &&
|
||||
(obj as any).unselect !== undefined &&
|
||||
isFocusableNode(obj)
|
||||
isFocusableNode(obj) &&
|
||||
typeof (obj as ISelectable).id === 'string' &&
|
||||
typeof (obj as ISelectable).workspace === 'object' &&
|
||||
typeof (obj as ISelectable).select === 'function' &&
|
||||
typeof (obj as ISelectable).unselect === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,5 +24,9 @@ export interface ISerializable {
|
||||
|
||||
/** Type guard that checks whether the given object is a ISerializable. */
|
||||
export function isSerializable(obj: any): obj is ISerializable {
|
||||
return obj.saveState !== undefined && obj.loadState !== undefined;
|
||||
return (
|
||||
obj &&
|
||||
typeof obj.saveState === 'function' &&
|
||||
typeof obj.loadState === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
||||
* @returns The first field or input of the given block, if any.
|
||||
*/
|
||||
getFirstChild(current: BlockSvg): IFocusableNode | null {
|
||||
const candidates = getBlockNavigationCandidates(current);
|
||||
const candidates = getBlockNavigationCandidates(current, true);
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
||||
return current.nextConnection?.targetBlock();
|
||||
} else if (current.outputConnection?.targetBlock()) {
|
||||
return navigateBlock(current, 1);
|
||||
} else if (current.getSurroundParent()) {
|
||||
return navigateBlock(current.getTopStackBlock(), 1);
|
||||
} else if (this.getParent(current) instanceof WorkspaceSvg) {
|
||||
return navigateStacks(current, 1);
|
||||
}
|
||||
@@ -111,14 +113,27 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
||||
* @param block The block to retrieve the navigable children of.
|
||||
* @returns A list of navigable/focusable children of the given block.
|
||||
*/
|
||||
function getBlockNavigationCandidates(block: BlockSvg): IFocusableNode[] {
|
||||
function getBlockNavigationCandidates(
|
||||
block: BlockSvg,
|
||||
forward: boolean,
|
||||
): IFocusableNode[] {
|
||||
const candidates: IFocusableNode[] = block.getIcons();
|
||||
|
||||
for (const input of block.inputList) {
|
||||
if (!input.isVisible()) continue;
|
||||
candidates.push(...input.fieldRow);
|
||||
if (input.connection?.targetBlock()) {
|
||||
candidates.push(input.connection.targetBlock() as BlockSvg);
|
||||
const connectedBlock = input.connection.targetBlock() as BlockSvg;
|
||||
if (input.connection.type === ConnectionType.NEXT_STATEMENT && !forward) {
|
||||
const lastStackBlock = connectedBlock
|
||||
.lastConnectionInStack(false)
|
||||
?.getSourceBlock();
|
||||
if (lastStackBlock) {
|
||||
candidates.push(lastStackBlock);
|
||||
}
|
||||
} else {
|
||||
candidates.push(connectedBlock);
|
||||
}
|
||||
} else if (input.connection?.type === ConnectionType.INPUT_VALUE) {
|
||||
candidates.push(input.connection as RenderedConnection);
|
||||
}
|
||||
@@ -174,11 +189,11 @@ export function navigateBlock(
|
||||
): IFocusableNode | null {
|
||||
const block =
|
||||
current instanceof BlockSvg
|
||||
? current.outputConnection.targetBlock()
|
||||
? (current.outputConnection?.targetBlock() ?? current.getSurroundParent())
|
||||
: current.getSourceBlock();
|
||||
if (!(block instanceof BlockSvg)) return null;
|
||||
|
||||
const candidates = getBlockNavigationCandidates(block);
|
||||
const candidates = getBlockNavigationCandidates(block, delta > 0);
|
||||
const currentIndex = candidates.indexOf(current);
|
||||
if (currentIndex === -1) return null;
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
*/
|
||||
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import {getFocusManager} from '../focus_manager.js';
|
||||
import {Icon} from '../icons/icon.js';
|
||||
import {MutatorIcon} from '../icons/mutator_icon.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
import {navigateBlock} from './block_navigation_policy.js';
|
||||
@@ -17,10 +19,18 @@ export class IconNavigationPolicy implements INavigationPolicy<Icon> {
|
||||
/**
|
||||
* Returns the first child of the given icon.
|
||||
*
|
||||
* @param _current The icon to return the first child of.
|
||||
* @param current The icon to return the first child of.
|
||||
* @returns Null.
|
||||
*/
|
||||
getFirstChild(_current: Icon): IFocusableNode | null {
|
||||
getFirstChild(current: Icon): IFocusableNode | null {
|
||||
if (
|
||||
current instanceof MutatorIcon &&
|
||||
current.bubbleIsVisible() &&
|
||||
getFocusManager().getFocusedNode() === current
|
||||
) {
|
||||
return current.getBubble()?.getWorkspace() ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import {BlockSvg} from '../block_svg.js';
|
||||
import {Field} from '../field.js';
|
||||
import {getFocusManager} from '../focus_manager.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import {isFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {Marker} from './marker.js';
|
||||
@@ -374,17 +373,8 @@ export class LineCursor extends Marker {
|
||||
*
|
||||
* @returns The current field, connection, or block the cursor is on.
|
||||
*/
|
||||
override getCurNode(): IFocusableNode | null {
|
||||
// Ensure the current node matches what's currently focused.
|
||||
const focused = getFocusManager().getFocusedNode();
|
||||
const block = this.getSourceBlockFromNode(focused);
|
||||
if (block && block.workspace === this.workspace) {
|
||||
// If the current focused node corresponds to a block then ensure that it
|
||||
// belongs to the correct workspace for this cursor.
|
||||
this.setCurNode(focused);
|
||||
}
|
||||
|
||||
return super.getCurNode();
|
||||
getCurNode(): IFocusableNode | null {
|
||||
return getFocusManager().getFocusedNode();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -395,12 +385,8 @@ export class LineCursor extends Marker {
|
||||
*
|
||||
* @param newNode The new location of the cursor.
|
||||
*/
|
||||
override setCurNode(newNode: IFocusableNode | null) {
|
||||
super.setCurNode(newNode);
|
||||
|
||||
if (isFocusableNode(newNode)) {
|
||||
getFocusManager().focusNode(newNode);
|
||||
}
|
||||
setCurNode(newNode: IFocusableNode) {
|
||||
getFocusManager().focusNode(newNode);
|
||||
|
||||
// Try to scroll cursor into view.
|
||||
if (newNode instanceof BlockSvg) {
|
||||
|
||||
@@ -62,7 +62,7 @@ export class WorkspaceNavigationPolicy
|
||||
* @returns True if the given workspace can be focused.
|
||||
*/
|
||||
isNavigable(current: WorkspaceSvg): boolean {
|
||||
return current.canBeFocused();
|
||||
return current.canBeFocused() && !current.isMutator;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,9 +64,8 @@ export class Navigator {
|
||||
getFirstChild(current: IFocusableNode): IFocusableNode | null {
|
||||
const result = this.get(current)?.getFirstChild(current);
|
||||
if (!result) return null;
|
||||
// If the child isn't navigable, don't traverse into it; check its peers.
|
||||
if (!this.get(result)?.isNavigable(result)) {
|
||||
return this.getNextSibling(result);
|
||||
return this.getFirstChild(result) || this.getNextSibling(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -8,19 +8,13 @@
|
||||
|
||||
import {BlockSvg} from './block_svg.js';
|
||||
import * as clipboard from './clipboard.js';
|
||||
import {RenderedWorkspaceComment} from './comments.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {getFocusManager} from './focus_manager.js';
|
||||
import {Gesture} from './gesture.js';
|
||||
import {
|
||||
ICopyable,
|
||||
ICopyData,
|
||||
isCopyable as isICopyable,
|
||||
} from './interfaces/i_copyable.js';
|
||||
import {
|
||||
IDeletable,
|
||||
isDeletable as isIDeletable,
|
||||
} from './interfaces/i_deletable.js';
|
||||
import {IDraggable, isDraggable} from './interfaces/i_draggable.js';
|
||||
import {ICopyData, isCopyable as isICopyable} from './interfaces/i_copyable.js';
|
||||
import {isDeletable as isIDeletable} from './interfaces/i_deletable.js';
|
||||
import {isDraggable} from './interfaces/i_draggable.js';
|
||||
import {IFocusableNode} from './interfaces/i_focusable_node.js';
|
||||
import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
@@ -100,74 +94,43 @@ export function registerDelete() {
|
||||
}
|
||||
|
||||
let copyData: ICopyData | null = null;
|
||||
let copyWorkspace: WorkspaceSvg | null = null;
|
||||
let copyCoords: Coordinate | null = null;
|
||||
|
||||
/**
|
||||
* Determine if a focusable node can be copied.
|
||||
*
|
||||
* Unfortunately the ICopyable interface doesn't include an isCopyable
|
||||
* method, so we must use some other criteria to make the decision.
|
||||
* Specifically,
|
||||
*
|
||||
* - It must be an ICopyable.
|
||||
* - So that a pasted copy can be manipluated and/or disposed of, it
|
||||
* must be both an IDraggable and an IDeletable.
|
||||
* - Additionally, both .isOwnMovable() and .isOwnDeletable() must return
|
||||
* true (i.e., the copy could be moved and deleted).
|
||||
*
|
||||
* TODO(#9098): Revise these criteria. The latter criteria prevents
|
||||
* shadow blocks from being copied; additionally, there are likely to
|
||||
* be other circumstances were it is desirable to allow movable /
|
||||
* copyable copies of a currently-unmovable / -copyable block to be
|
||||
* made.
|
||||
* This will use the isCopyable method if the node implements it, otherwise
|
||||
* it will fall back to checking if the node is deletable and draggable not
|
||||
* considering the workspace's edit state.
|
||||
*
|
||||
* @param focused The focused object.
|
||||
*/
|
||||
function isCopyable(
|
||||
focused: IFocusableNode,
|
||||
): focused is ICopyable<ICopyData> & IDeletable & IDraggable {
|
||||
if (!(focused instanceof BlockSvg)) return false;
|
||||
return (
|
||||
isICopyable(focused) &&
|
||||
isIDeletable(focused) &&
|
||||
focused.isOwnDeletable() &&
|
||||
isDraggable(focused) &&
|
||||
focused.isOwnMovable()
|
||||
);
|
||||
function isCopyable(focused: IFocusableNode): boolean {
|
||||
if (!isICopyable(focused) || !isIDeletable(focused) || !isDraggable(focused))
|
||||
return false;
|
||||
if (focused.isCopyable) {
|
||||
return focused.isCopyable();
|
||||
} else if (
|
||||
focused instanceof BlockSvg ||
|
||||
focused instanceof RenderedWorkspaceComment
|
||||
) {
|
||||
return focused.isOwnDeletable() && focused.isOwnMovable();
|
||||
}
|
||||
// This isn't a class Blockly knows about, so fall back to the stricter
|
||||
// checks for deletable and movable.
|
||||
return focused.isDeletable() && focused.isMovable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a focusable node can be cut.
|
||||
*
|
||||
* Unfortunately the ICopyable interface doesn't include an isCuttable
|
||||
* method, so we must use some other criteria to make the decision.
|
||||
* Specifically,
|
||||
*
|
||||
* - It must be an ICopyable.
|
||||
* - So that a pasted copy can be manipluated and/or disposed of, it
|
||||
* must be both an IDraggable and an IDeletable.
|
||||
* - Additionally, both .isMovable() and .isDeletable() must return
|
||||
* true (i.e., can currently be moved and deleted). This is the main
|
||||
* difference with isCopyable.
|
||||
*
|
||||
* TODO(#9098): Revise these criteria. The latter criteria prevents
|
||||
* shadow blocks from being copied; additionally, there are likely to
|
||||
* be other circumstances were it is desirable to allow movable /
|
||||
* copyable copies of a currently-unmovable / -copyable block to be
|
||||
* made.
|
||||
* This will check if the node can be both copied and deleted in its current
|
||||
* workspace.
|
||||
*
|
||||
* @param focused The focused object.
|
||||
*/
|
||||
function isCuttable(focused: IFocusableNode): boolean {
|
||||
if (!(focused instanceof BlockSvg)) return false;
|
||||
return (
|
||||
isICopyable(focused) &&
|
||||
isIDeletable(focused) &&
|
||||
focused.isDeletable() &&
|
||||
isDraggable(focused) &&
|
||||
focused.isMovable()
|
||||
);
|
||||
return isCopyable(focused) && isIDeletable(focused) && focused.isDeletable();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,7 +148,6 @@ export function registerCopy() {
|
||||
name: names.COPY,
|
||||
preconditionFn(workspace, scope) {
|
||||
const focused = scope.focusedNode;
|
||||
if (!(focused instanceof BlockSvg)) return false;
|
||||
|
||||
const targetWorkspace = workspace.isFlyout
|
||||
? workspace.targetWorkspace
|
||||
@@ -193,7 +155,6 @@ export function registerCopy() {
|
||||
return (
|
||||
!!focused &&
|
||||
!!targetWorkspace &&
|
||||
!targetWorkspace.isReadOnly() &&
|
||||
!targetWorkspace.isDragging() &&
|
||||
!getFocusManager().ephemeralFocusTaken() &&
|
||||
isCopyable(focused)
|
||||
@@ -205,21 +166,17 @@ export function registerCopy() {
|
||||
e.preventDefault();
|
||||
|
||||
const focused = scope.focusedNode;
|
||||
if (!focused || !isCopyable(focused)) return false;
|
||||
let targetWorkspace: WorkspaceSvg | null =
|
||||
focused.workspace instanceof WorkspaceSvg
|
||||
? focused.workspace
|
||||
: workspace;
|
||||
targetWorkspace = targetWorkspace.isFlyout
|
||||
? targetWorkspace.targetWorkspace
|
||||
: targetWorkspace;
|
||||
if (!focused || !isICopyable(focused) || !isCopyable(focused))
|
||||
return false;
|
||||
const targetWorkspace = workspace.isFlyout
|
||||
? workspace.targetWorkspace
|
||||
: workspace;
|
||||
if (!targetWorkspace) return false;
|
||||
|
||||
if (!focused.workspace.isFlyout) {
|
||||
targetWorkspace.hideChaff();
|
||||
}
|
||||
copyData = focused.toCopyData();
|
||||
copyWorkspace = targetWorkspace;
|
||||
copyCoords =
|
||||
isDraggable(focused) && focused.workspace == targetWorkspace
|
||||
? focused.getRelativeToSurfaceXY()
|
||||
@@ -256,27 +213,20 @@ export function registerCut() {
|
||||
},
|
||||
callback(workspace, e, shortcut, scope) {
|
||||
const focused = scope.focusedNode;
|
||||
if (!focused || !isCuttable(focused) || !isICopyable(focused)) {
|
||||
return false;
|
||||
}
|
||||
copyData = focused.toCopyData();
|
||||
copyCoords = isDraggable(focused)
|
||||
? focused.getRelativeToSurfaceXY()
|
||||
: null;
|
||||
|
||||
if (focused instanceof BlockSvg) {
|
||||
copyData = focused.toCopyData();
|
||||
copyWorkspace = workspace;
|
||||
copyCoords = focused.getRelativeToSurfaceXY();
|
||||
focused.checkAndDelete();
|
||||
return true;
|
||||
} else if (
|
||||
isIDeletable(focused) &&
|
||||
focused.isDeletable() &&
|
||||
isICopyable(focused)
|
||||
) {
|
||||
copyData = focused.toCopyData();
|
||||
copyWorkspace = workspace;
|
||||
copyCoords = isDraggable(focused)
|
||||
? focused.getRelativeToSurfaceXY()
|
||||
: null;
|
||||
} else if (isIDeletable(focused)) {
|
||||
focused.dispose();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return !!copyData;
|
||||
},
|
||||
keyCodes: [ctrlX, metaX],
|
||||
};
|
||||
@@ -310,7 +260,11 @@ export function registerPaste() {
|
||||
);
|
||||
},
|
||||
callback(workspace: WorkspaceSvg, e: Event) {
|
||||
if (!copyData || !copyWorkspace) return false;
|
||||
if (!copyData) return false;
|
||||
const targetWorkspace = workspace.isFlyout
|
||||
? workspace.targetWorkspace
|
||||
: workspace;
|
||||
if (!targetWorkspace || targetWorkspace.isReadOnly()) return false;
|
||||
|
||||
if (e instanceof PointerEvent) {
|
||||
// The event that triggers a shortcut would conventionally be a KeyboardEvent.
|
||||
@@ -319,19 +273,19 @@ export function registerPaste() {
|
||||
// at the mouse coordinates where the menu was opened, and this PointerEvent
|
||||
// is where the menu was opened.
|
||||
const mouseCoords = svgMath.screenToWsCoordinates(
|
||||
copyWorkspace,
|
||||
targetWorkspace,
|
||||
new Coordinate(e.clientX, e.clientY),
|
||||
);
|
||||
return !!clipboard.paste(copyData, copyWorkspace, mouseCoords);
|
||||
return !!clipboard.paste(copyData, targetWorkspace, mouseCoords);
|
||||
}
|
||||
|
||||
if (!copyCoords) {
|
||||
// If we don't have location data about the original copyable, let the
|
||||
// paster determine position.
|
||||
return !!clipboard.paste(copyData, copyWorkspace);
|
||||
return !!clipboard.paste(copyData, targetWorkspace);
|
||||
}
|
||||
|
||||
const {left, top, width, height} = copyWorkspace
|
||||
const {left, top, width, height} = targetWorkspace
|
||||
.getMetricsManager()
|
||||
.getViewMetrics(true);
|
||||
const viewportRect = new Rect(top, top + height, left, left + width);
|
||||
@@ -339,12 +293,12 @@ export function registerPaste() {
|
||||
if (viewportRect.contains(copyCoords.x, copyCoords.y)) {
|
||||
// If the original copyable is inside the viewport, let the paster
|
||||
// determine position.
|
||||
return !!clipboard.paste(copyData, copyWorkspace);
|
||||
return !!clipboard.paste(copyData, targetWorkspace);
|
||||
}
|
||||
|
||||
// Otherwise, paste in the middle of the viewport.
|
||||
const centerCoords = new Coordinate(left + width / 2, top + height / 2);
|
||||
return !!clipboard.paste(copyData, copyWorkspace, centerCoords);
|
||||
return !!clipboard.paste(copyData, targetWorkspace, centerCoords);
|
||||
},
|
||||
keyCodes: [ctrlV, metaV],
|
||||
};
|
||||
@@ -390,12 +344,12 @@ export function registerUndo() {
|
||||
*/
|
||||
export function registerRedo() {
|
||||
const ctrlShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
|
||||
KeyCodes.SHIFT,
|
||||
KeyCodes.CTRL,
|
||||
KeyCodes.SHIFT,
|
||||
]);
|
||||
const metaShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
|
||||
KeyCodes.SHIFT,
|
||||
KeyCodes.META,
|
||||
KeyCodes.SHIFT,
|
||||
]);
|
||||
// Ctrl-y is redo in Windows. Command-y is never valid on Macs.
|
||||
const ctrlY = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Y, [
|
||||
|
||||
@@ -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 {COMMENT_EDITOR_FOCUS_IDENTIFIER} from './comments/comment_editor.js';
|
||||
import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
|
||||
import {WorkspaceComment} from './comments/workspace_comment.js';
|
||||
import * as common from './common.js';
|
||||
@@ -41,6 +42,7 @@ import type {FlyoutButton} from './flyout_button.js';
|
||||
import {getFocusManager} from './focus_manager.js';
|
||||
import {Gesture} from './gesture.js';
|
||||
import {Grid} from './grid.js';
|
||||
import {MutatorIcon} from './icons/mutator_icon.js';
|
||||
import {isAutoHideable} from './interfaces/i_autohideable.js';
|
||||
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||
import {IContextMenu} from './interfaces/i_contextmenu.js';
|
||||
@@ -2680,7 +2682,7 @@ export class WorkspaceSvg
|
||||
|
||||
/** See IFocusableNode.getFocusableTree. */
|
||||
getFocusableTree(): IFocusableTree {
|
||||
return this;
|
||||
return (this.isMutator && this.options.parentWorkspace) || this;
|
||||
}
|
||||
|
||||
/** See IFocusableNode.onNodeFocus. */
|
||||
@@ -2710,7 +2712,42 @@ export class WorkspaceSvg
|
||||
|
||||
/** See IFocusableTree.getNestedTrees. */
|
||||
getNestedTrees(): Array<IFocusableTree> {
|
||||
return [];
|
||||
const nestedWorkspaces = this.getAllBlocks()
|
||||
.map((block) => block.getIcons())
|
||||
.flat()
|
||||
.filter(
|
||||
(icon): icon is MutatorIcon =>
|
||||
icon instanceof MutatorIcon && icon.bubbleIsVisible(),
|
||||
)
|
||||
.map((icon) => icon.getBubble()?.getWorkspace())
|
||||
.filter((workspace) => !!workspace);
|
||||
|
||||
const ownFlyout = this.getFlyout(true);
|
||||
if (ownFlyout) {
|
||||
nestedWorkspaces.push(ownFlyout.getWorkspace());
|
||||
}
|
||||
|
||||
return nestedWorkspaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for searching for a specific workspace comment.
|
||||
* We can't use this.getWorkspaceCommentById because the workspace
|
||||
* comment ids might not be globally unique, but the id assigned to
|
||||
* the focusable element for the comment should be.
|
||||
*/
|
||||
private searchForWorkspaceComment(
|
||||
id: string,
|
||||
): RenderedWorkspaceComment | undefined {
|
||||
for (const comment of this.getTopComments()) {
|
||||
if (
|
||||
comment instanceof RenderedWorkspaceComment &&
|
||||
comment.canBeFocused() &&
|
||||
comment.getFocusableElement().id === id
|
||||
) {
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** See IFocusableTree.lookUpFocusableNode. */
|
||||
@@ -2757,21 +2794,29 @@ export class WorkspaceSvg
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search for a specific workspace comment editor
|
||||
// (only if id seems like it is one).
|
||||
const commentEditorIndicator = id.indexOf(COMMENT_EDITOR_FOCUS_IDENTIFIER);
|
||||
if (commentEditorIndicator !== -1) {
|
||||
const commentId = id.substring(0, commentEditorIndicator);
|
||||
const comment = this.searchForWorkspaceComment(commentId);
|
||||
if (comment) {
|
||||
return comment.getEditorFocusableNode();
|
||||
}
|
||||
}
|
||||
|
||||
// Search for a specific block.
|
||||
// Don't use `getBlockById` because the block ID is not guaranteeed
|
||||
// to be globally unique, but the ID on the focusable element is.
|
||||
const block = this.getAllBlocks(false).find(
|
||||
(block) => block.getFocusableElement().id === id,
|
||||
);
|
||||
if (block) return block;
|
||||
|
||||
// Search for a workspace comment (semi-expensive).
|
||||
for (const comment of this.getTopComments()) {
|
||||
if (
|
||||
comment instanceof RenderedWorkspaceComment &&
|
||||
comment.canBeFocused() &&
|
||||
comment.getFocusableElement().id === id
|
||||
) {
|
||||
return comment;
|
||||
}
|
||||
const comment = this.searchForWorkspaceComment(id);
|
||||
if (comment) {
|
||||
return comment;
|
||||
}
|
||||
|
||||
// Search for icons and bubbles (which requires an expensive getAllBlocks).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"author": "Ellen Spertus <ellen.spertus@gmail.com>",
|
||||
"lastupdated": "2025-04-21 10:42:10.549634",
|
||||
"lastupdated": "2025-06-17 15:36:41.845826",
|
||||
"locale": "en",
|
||||
"messagedocumentation" : "qqq"
|
||||
},
|
||||
@@ -398,22 +398,8 @@
|
||||
"COLLAPSED_WARNINGS_WARNING": "Collapsed blocks contain warnings.",
|
||||
"DIALOG_OK": "OK",
|
||||
"DIALOG_CANCEL": "Cancel",
|
||||
"DELETE_SHORTCUT": "Delete block (%1)",
|
||||
"DELETE_KEY": "Del",
|
||||
"EDIT_BLOCK_CONTENTS": "Edit Block contents (%1)",
|
||||
"INSERT_BLOCK": "Insert Block (%1)",
|
||||
"START_MOVE": "Start move",
|
||||
"FINISH_MOVE": "Finish move",
|
||||
"ABORT_MOVE": "Abort move",
|
||||
"MOVE_LEFT_CONSTRAINED": "Move left, constrained",
|
||||
"MOVE_RIGHT_CONSTRAINED": "Move right constrained",
|
||||
"MOVE_UP_CONSTRAINED": "Move up, constrained",
|
||||
"MOVE_DOWN_CONSTRAINED": "Move down constrained",
|
||||
"MOVE_LEFT_UNCONSTRAINED": "Move left, unconstrained",
|
||||
"MOVE_RIGHT_UNCONSTRAINED": "Move right, unconstrained",
|
||||
"MOVE_UP_UNCONSTRAINED": "Move up unconstrained",
|
||||
"MOVE_DOWN_UNCONSTRAINED": "Move down, unconstrained",
|
||||
"MOVE_BLOCK": "Move Block (%1)",
|
||||
"EDIT_BLOCK_CONTENTS": "Edit Block contents",
|
||||
"MOVE_BLOCK": "Move Block",
|
||||
"WINDOWS": "Windows",
|
||||
"MAC_OS": "macOS",
|
||||
"CHROME_OS": "ChromeOS",
|
||||
@@ -423,11 +409,15 @@
|
||||
"COMMAND_KEY": "⌘ Command",
|
||||
"OPTION_KEY": "⌥ Option",
|
||||
"ALT_KEY": "Alt",
|
||||
"CUT_SHORTCUT": "Cut (%1)",
|
||||
"COPY_SHORTCUT": "Copy (%1)",
|
||||
"PASTE_SHORTCUT": "Paste (%1)",
|
||||
"CUT_SHORTCUT": "Cut",
|
||||
"COPY_SHORTCUT": "Copy",
|
||||
"PASTE_SHORTCUT": "Paste",
|
||||
"HELP_PROMPT": "Press %1 for help on keyboard controls",
|
||||
"SHORTCUTS_GENERAL": "General",
|
||||
"SHORTCUTS_EDITING": "Editing",
|
||||
"SHORTCUTS_CODE_NAVIGATION": "Code navigation"
|
||||
"SHORTCUTS_CODE_NAVIGATION": "Code navigation",
|
||||
"KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT": "Hold %1 and use arrow keys to move freely, then %2 to accept the position",
|
||||
"KEYBOARD_NAV_CONSTRAINED_MOVE_HINT": "Use the arrow keys to move, then %1 to accept the position",
|
||||
"KEYBOARD_NAV_COPIED_HINT": "Copied. Press %1 to paste.",
|
||||
"KEYBOARD_NAV_CUT_HINT": "Cut. Press %1 to paste."
|
||||
}
|
||||
|
||||
@@ -405,21 +405,7 @@
|
||||
"COLLAPSED_WARNINGS_WARNING": "warning - This appears if the user collapses a block, and blocks inside that block have warnings attached to them. It should inform the user that the block they collapsed contains blocks that have warnings.",
|
||||
"DIALOG_OK": "button label - Pressing this button closes help information.\n{{Identical|OK}}",
|
||||
"DIALOG_CANCEL": "button label - Pressing this button cancels a proposed action.\n{{Identical|Cancel}}",
|
||||
"DELETE_SHORTCUT": "menu label - Contextual menu item that deletes the focused block.",
|
||||
"DELETE_KEY": "menu label - Keyboard shortcut for the Delete key, shown at the end of a menu item that deletes the focused block.",
|
||||
"EDIT_BLOCK_CONTENTS": "menu label - Contextual menu item that moves the keyboard navigation cursor into a subitem of the focused block.",
|
||||
"INSERT_BLOCK": "menu label - Contextual menu item that prompts the user to choose a block to insert into the program at the focused location.",
|
||||
"START_MOVE": "keyboard shortcut label - Contextual menu item that starts a keyboard-driven move of the focused block.",
|
||||
"FINISH_MOVE": "keyboard shortcut label - Contextual menu item that ends a keyboard-driven move of the focused block.",
|
||||
"ABORT_MOVE": "keyboard shortcut label - Contextual menu item that ends a keyboard-drive move of the focused block by returning it to its original location.",
|
||||
"MOVE_LEFT_CONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block to the next valid location to the left.",
|
||||
"MOVE_RIGHT_CONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block to the next valid location to the right.",
|
||||
"MOVE_UP_CONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block to the next valid location above it.",
|
||||
"MOVE_DOWN_CONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block to the next valid location below it.",
|
||||
"MOVE_LEFT_UNCONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block freely to the left.",
|
||||
"MOVE_RIGHT_UNCONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block freely to the right.",
|
||||
"MOVE_UP_UNCONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block freely upwards.",
|
||||
"MOVE_DOWN_UNCONSTRAINED": "keyboard shortcut label - Description of shortcut that moves a block freely downwards.",
|
||||
"MOVE_BLOCK": "menu label - Contextual menu item that starts a keyboard-driven block move.",
|
||||
"WINDOWS": "Name of the Microsoft Windows operating system displayed in a list of keyboard shortcuts.",
|
||||
"MAC_OS": "Name of the Apple macOS operating system displayed in a list of keyboard shortcuts,",
|
||||
@@ -436,5 +422,9 @@
|
||||
"HELP_PROMPT": "Alert message shown to prompt users to review available keyboard shortcuts.",
|
||||
"SHORTCUTS_GENERAL": "shortcut list section header - Label for general purpose keyboard shortcuts.",
|
||||
"SHORTCUTS_EDITING": "shortcut list section header - Label for keyboard shortcuts related to editing a workspace.",
|
||||
"SHORTCUTS_CODE_NAVIGATION": "shortcut list section header - Label for keyboard shortcuts related to moving around the workspace."
|
||||
"SHORTCUTS_CODE_NAVIGATION": "shortcut list section header - Label for keyboard shortcuts related to moving around the workspace.",
|
||||
"KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT": "Message shown to inform users how to move blocks to arbitrary locations with the keyboard.",
|
||||
"KEYBOARD_NAV_CONSTRAINED_MOVE_HINT": "Message shown to inform users how to move blocks with the keyboard.",
|
||||
"KEYBOARD_NAV_COPIED_HINT": "Message shown when an item is copied in keyboard navigation mode.",
|
||||
"KEYBOARD_NAV_CUT_HINT": "Message shown when an item is cut in keyboard navigation mode."
|
||||
}
|
||||
|
||||
@@ -1618,68 +1618,13 @@ Blockly.Msg.DIALOG_OK = 'OK';
|
||||
/// button label - Pressing this button cancels a proposed action.\n{{Identical|Cancel}}
|
||||
Blockly.Msg.DIALOG_CANCEL = 'Cancel';
|
||||
|
||||
/** @type {string} */
|
||||
/// menu label - Contextual menu item that deletes the focused block.
|
||||
Blockly.Msg.DELETE_SHORTCUT = 'Delete block (%1)';
|
||||
/** @type {string} */
|
||||
/// menu label - Keyboard shortcut for the Delete key, shown at the end of a
|
||||
/// menu item that deletes the focused block.
|
||||
Blockly.Msg.DELETE_KEY = 'Del';
|
||||
/** @type {string} */
|
||||
/// menu label - Contextual menu item that moves the keyboard navigation cursor
|
||||
/// into a subitem of the focused block.
|
||||
Blockly.Msg.EDIT_BLOCK_CONTENTS = 'Edit Block contents (%1)';
|
||||
/** @type {string} */
|
||||
/// menu label - Contextual menu item that prompts the user to choose a block to
|
||||
/// insert into the program at the focused location.
|
||||
Blockly.Msg.INSERT_BLOCK = 'Insert Block (%1)';
|
||||
/** @type {string} */
|
||||
/// keyboard shortcut label - Contextual menu item that starts a keyboard-driven
|
||||
/// move of the focused block.
|
||||
Blockly.Msg.START_MOVE = 'Start move';
|
||||
/** @type {string} */
|
||||
/// keyboard shortcut label - Contextual menu item that ends a keyboard-driven
|
||||
/// move of the focused block.
|
||||
Blockly.Msg.FINISH_MOVE = 'Finish move';
|
||||
/** @type {string} */
|
||||
/// keyboard shortcut label - Contextual menu item that ends a keyboard-drive
|
||||
/// move of the focused block by returning it to its original location.
|
||||
Blockly.Msg.ABORT_MOVE = 'Abort move';
|
||||
/** @type {string} */
|
||||
/// keyboard shortcut label - Description of shortcut that moves a block to the
|
||||
/// next valid location to the left.
|
||||
Blockly.Msg.MOVE_LEFT_CONSTRAINED = 'Move left, constrained';
|
||||
/** @type {string} */
|
||||
/// keyboard shortcut label - Description of shortcut that moves a block to the
|
||||
/// next valid location to the right.
|
||||
Blockly.Msg.MOVE_RIGHT_CONSTRAINED = 'Move right constrained';
|
||||
/** @type {string} */
|
||||
/// keyboard shortcut label - Description of shortcut that moves a block to the
|
||||
/// next valid location above it.
|
||||
Blockly.Msg.MOVE_UP_CONSTRAINED = 'Move up, constrained';
|
||||
/** @type {string} */
|
||||
/// keyboard shortcut label - Description of shortcut that moves a block to the
|
||||
/// next valid location below it.
|
||||
Blockly.Msg.MOVE_DOWN_CONSTRAINED = 'Move down constrained';
|
||||
/** @type {string} */
|
||||
/// keyboard shortcut label - Description of shortcut that moves a block freely
|
||||
/// to the left.
|
||||
Blockly.Msg.MOVE_LEFT_UNCONSTRAINED = 'Move left, unconstrained';
|
||||
/** @type {string} */
|
||||
/// keyboard shortcut label - Description of shortcut that moves a block freely
|
||||
/// to the right.
|
||||
Blockly.Msg.MOVE_RIGHT_UNCONSTRAINED = 'Move right, unconstrained';
|
||||
/** @type {string} */
|
||||
/// keyboard shortcut label - Description of shortcut that moves a block freely
|
||||
/// upwards.
|
||||
Blockly.Msg.MOVE_UP_UNCONSTRAINED = 'Move up unconstrained';
|
||||
/** @type {string} */
|
||||
/// keyboard shortcut label - Description of shortcut that moves a block freely
|
||||
/// downwards.
|
||||
Blockly.Msg.MOVE_DOWN_UNCONSTRAINED = 'Move down, unconstrained';
|
||||
Blockly.Msg.EDIT_BLOCK_CONTENTS = 'Edit Block contents';
|
||||
/** @type {string} */
|
||||
/// menu label - Contextual menu item that starts a keyboard-driven block move.
|
||||
Blockly.Msg.MOVE_BLOCK = 'Move Block (%1)';
|
||||
Blockly.Msg.MOVE_BLOCK = 'Move Block';
|
||||
/** @type {string} */
|
||||
/// Name of the Microsoft Windows operating system displayed in a list of
|
||||
/// keyboard shortcuts.
|
||||
@@ -1714,13 +1659,13 @@ Blockly.Msg.OPTION_KEY = '⌥ Option';
|
||||
Blockly.Msg.ALT_KEY = 'Alt';
|
||||
/** @type {string} */
|
||||
/// menu label - Contextual menu item that cuts the focused item.
|
||||
Blockly.Msg.CUT_SHORTCUT = 'Cut (%1)';
|
||||
Blockly.Msg.CUT_SHORTCUT = 'Cut';
|
||||
/** @type {string} */
|
||||
/// menu label - Contextual menu item that copies the focused item.
|
||||
Blockly.Msg.COPY_SHORTCUT = 'Copy (%1)';
|
||||
Blockly.Msg.COPY_SHORTCUT = 'Copy';
|
||||
/** @type {string} */
|
||||
/// menu label - Contextual menu item that pastes the previously copied item.
|
||||
Blockly.Msg.PASTE_SHORTCUT = 'Paste (%1)';
|
||||
Blockly.Msg.PASTE_SHORTCUT = 'Paste';
|
||||
/** @type {string} */
|
||||
/// Alert message shown to prompt users to review available keyboard shortcuts.
|
||||
Blockly.Msg.HELP_PROMPT = 'Press %1 for help on keyboard controls';
|
||||
@@ -1735,3 +1680,16 @@ Blockly.Msg.SHORTCUTS_EDITING = 'Editing'
|
||||
/// shortcut list section header - Label for keyboard shortcuts related to
|
||||
/// moving around the workspace.
|
||||
Blockly.Msg.SHORTCUTS_CODE_NAVIGATION = 'Code navigation';
|
||||
/** @type {string} */
|
||||
/// Message shown to inform users how to move blocks to arbitrary locations
|
||||
/// with the keyboard.
|
||||
Blockly.Msg.KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT = 'Hold %1 and use arrow keys to move freely, then %2 to accept the position';
|
||||
/** @type {string} */
|
||||
/// Message shown to inform users how to move blocks with the keyboard.
|
||||
Blockly.Msg.KEYBOARD_NAV_CONSTRAINED_MOVE_HINT = 'Use the arrow keys to move, then %1 to accept the position';
|
||||
/** @type {string} */
|
||||
/// Message shown when an item is copied in keyboard navigation mode.
|
||||
Blockly.Msg.KEYBOARD_NAV_COPIED_HINT = 'Copied. Press %1 to paste.';
|
||||
/** @type {string} */
|
||||
/// Message shown when an item is cut in keyboard navigation mode.
|
||||
Blockly.Msg.KEYBOARD_NAV_CUT_HINT = 'Cut. Press %1 to paste.';
|
||||
284
package-lock.json
generated
284
package-lock.json
generated
@@ -101,17 +101,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/dev-tools": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-9.0.0.tgz",
|
||||
"integrity": "sha512-c2JJbj5Q9mGdy0iUvE5OBOl1zmSMJrSokORgnmrhxGCiJ6QexPGCsi1QAn6uzpUtGKjhpnEAQ6+jX7ROZe7QQg==",
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-9.0.1.tgz",
|
||||
"integrity": "sha512-OnY24Up00owts0VtOaokUmOQdzH+K1PNcr3LC3huwa9PO0TlKiXTq4V5OuIqBS++enyj93gXQ8PhvFGudkogTQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@blockly/block-test": "^7.0.0",
|
||||
"@blockly/theme-dark": "^8.0.0",
|
||||
"@blockly/theme-deuteranopia": "^7.0.0",
|
||||
"@blockly/theme-highcontrast": "^7.0.0",
|
||||
"@blockly/theme-tritanopia": "^7.0.0",
|
||||
"@blockly/block-test": "^7.0.1",
|
||||
"@blockly/theme-dark": "^8.0.1",
|
||||
"@blockly/theme-deuteranopia": "^7.0.1",
|
||||
"@blockly/theme-highcontrast": "^7.0.1",
|
||||
"@blockly/theme-tritanopia": "^7.0.1",
|
||||
"chai": "^4.2.0",
|
||||
"dat.gui": "^0.7.7",
|
||||
"lodash.assign": "^4.2.0",
|
||||
@@ -127,9 +127,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/dev-tools/node_modules/@blockly/block-test": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.0.tgz",
|
||||
"integrity": "sha512-Y+Iwg1hHmOaqXveTOiZNXHH+jNBP+LC5L8ZxKKWeO8aB9DZD5G2hgApHfLaxeZzqnCl8zspvGnrrlFy9foEdWw==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.1.tgz",
|
||||
"integrity": "sha512-w91ZZbpJDKGQJVO7gKqQaM17ffcsW1ktrnSTz/OpDw5R4H+1q05NgWO5gYzGPzLfFdvPcrkc0v00KhD4UG7BRA==",
|
||||
"dev": true,
|
||||
"license": "Apache 2.0",
|
||||
"engines": {
|
||||
@@ -209,9 +209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/theme-dark": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-8.0.0.tgz",
|
||||
"integrity": "sha512-Fq8ifjCwbJW305Su7SNBP8jXs4h1hp2EdQ9cMGOCr/racRIYfDRRBqjy0ZRLLqI7BsgZKxKy6Aa+OjgWEKeKfw==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-8.0.1.tgz",
|
||||
"integrity": "sha512-0Di3WIUwCVQw7jK9myUf/J+4oHLADWc8YxeF40KQgGsyulVrVnYipwtBolj+wxq2xjxIkqgvctAN3BdvM4mynA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -222,9 +222,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/theme-deuteranopia": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-7.0.0.tgz",
|
||||
"integrity": "sha512-zKhlnD/AF3MR9+Rlwus3vAPq8gwCZaZ08VEupvz5b98mk36suRlIrQanM8HVLGcozxiEvUNrTNOGO5kj8PeTWA==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-7.0.1.tgz",
|
||||
"integrity": "sha512-V05Hk2hzQZict47LfzDdSTP+J5HlYiF7de/8LR/bsRQB/ft7UUTraqDLIivYc9gL2alsVtKzq/yFs9wi7FMAqQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -235,9 +235,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/theme-highcontrast": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-7.0.0.tgz",
|
||||
"integrity": "sha512-6Apkw5iUlOq1DoOJgwsfo8Iha2OkxXMSNHqb8ZVVmUhCHjce0XMXgq1Rqty/2l/C2AKB+WWLZEWxOyGWYrQViQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-7.0.1.tgz",
|
||||
"integrity": "sha512-dMhysbXf8QtHxuhI1EY5GdZErlfEhjpCogwfzglDKSu8MF2C+5qzOQBxKmqfnEYJl6G9B2HNGw+mEaUo8oel6Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -260,9 +260,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/theme-tritanopia": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-7.0.0.tgz",
|
||||
"integrity": "sha512-22TFAuY8ilKsQomDC8GXMHsCfdR8l75yPPFl6AOCcok2FJLkiyhjGpAy2cNexA9P2xP/rW7vdsG3wC8ukWihUA==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-7.0.1.tgz",
|
||||
"integrity": "sha512-eLqPCmW6xvSYvyTFFE5uz0Bw806LxOmaQrCOzbUywkT41s2ITP06OP1BVQrHdkZSt5whipZYpB1RMGxYxS/Bpw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -383,17 +383,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@es-joy/jsdoccomment": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz",
|
||||
"integrity": "sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==",
|
||||
"version": "0.50.2",
|
||||
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.50.2.tgz",
|
||||
"integrity": "sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.6",
|
||||
"@typescript-eslint/types": "^8.11.0",
|
||||
"comment-parser": "1.4.1",
|
||||
"esquery": "^1.6.0",
|
||||
"jsdoc-type-pratt-parser": "~4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
@@ -714,10 +717,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@hyperjump/browser": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@hyperjump/browser/-/browser-1.1.6.tgz",
|
||||
"integrity": "sha512-i27uPV7SxK1GOn7TLTRxTorxchYa5ur9JHgtl6TxZ1MHuyb9ROAnXxEeu4q4H1836Xb7lL2PGPsaa5Jl3p+R6g==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@hyperjump/browser/-/browser-1.3.1.tgz",
|
||||
"integrity": "sha512-Le5XZUjnVqVjkgLYv6yyWgALat/0HpB1XaCPuCZ+GCFki9NvXloSZITIJ0H+wRW7mb9At1SxvohKBbNQbrr/cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hyperjump/json-pointer": "^1.1.0",
|
||||
"@hyperjump/uri": "^1.2.0",
|
||||
@@ -743,9 +747,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@hyperjump/json-schema": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@hyperjump/json-schema/-/json-schema-1.11.0.tgz",
|
||||
"integrity": "sha512-gX1YNObOybUW6tgJjvb1lomNbI/VnY+EBPokmEGy9Lk8cgi+gE0vXhX1XDgIpUUA4UXfgHEn5I1mga5vHgOttg==",
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@hyperjump/json-schema/-/json-schema-1.15.1.tgz",
|
||||
"integrity": "sha512-/NtriODPtJ+4nqewSksw3YtcINXy1C2TraFuhah/IfSdwgBUas0XNCHJz9mXcniR7/2nCUSFMZg9A3wKo3i0iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1212,15 +1216,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgr/core": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
|
||||
"integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz",
|
||||
"integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/@promptbook/utils": {
|
||||
@@ -3176,6 +3181,7 @@
|
||||
"resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz",
|
||||
"integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
@@ -3693,9 +3699,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -3834,10 +3841,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
|
||||
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
@@ -4088,12 +4096,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
|
||||
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
@@ -4301,23 +4303,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsdoc": {
|
||||
"version": "50.6.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.9.tgz",
|
||||
"integrity": "sha512-7/nHu3FWD4QRG8tCVqcv+BfFtctUtEDWc29oeDXB4bwmDM2/r1ndl14AG/2DUntdqH7qmpvdemJKwb3R97/QEw==",
|
||||
"version": "50.7.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.7.1.tgz",
|
||||
"integrity": "sha512-XBnVA5g2kUVokTNUiE1McEPse5n9/mNUmuJcx52psT6zBs2eVcXSmQBvjfa7NZdfLVSy3u1pEDDUxoxpwy89WA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@es-joy/jsdoccomment": "~0.49.0",
|
||||
"@es-joy/jsdoccomment": "~0.50.2",
|
||||
"are-docs-informative": "^0.0.2",
|
||||
"comment-parser": "1.4.1",
|
||||
"debug": "^4.3.6",
|
||||
"debug": "^4.4.1",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"espree": "^10.1.0",
|
||||
"espree": "^10.3.0",
|
||||
"esquery": "^1.6.0",
|
||||
"parse-imports": "^2.1.1",
|
||||
"semver": "^7.6.3",
|
||||
"spdx-expression-parse": "^4.0.0",
|
||||
"synckit": "^0.9.1"
|
||||
"parse-imports-exports": "^0.2.4",
|
||||
"semver": "^7.7.2",
|
||||
"spdx-expression-parse": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -4327,10 +4328,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsdoc/node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
@@ -4349,14 +4351,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz",
|
||||
"integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.0.tgz",
|
||||
"integrity": "sha512-8qsOYwkkGrahrgoUv76NZi23koqXOGiiEzXMrT8Q7VcYaUISR+5MorIUxfWqYXN0fN/31WbSrxCxFkVQ43wwrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.0",
|
||||
"synckit": "^0.11.0"
|
||||
"synckit": "^0.11.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
@@ -4379,43 +4381,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier/node_modules/@pkgr/core": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz",
|
||||
"integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier/node_modules/synckit": {
|
||||
"version": "0.11.4",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz",
|
||||
"integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.2.3",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
|
||||
@@ -5476,9 +5441,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
|
||||
"integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
|
||||
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -6661,6 +6626,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz",
|
||||
"integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
@@ -7298,29 +7264,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-11.3.0.tgz",
|
||||
"integrity": "sha512-J0RLIM89xi8y6l77bgbX+03PeBRDQCOVQpnwOcCN7b8hCmbh6JvGI2ZDJ5WMoHz+IaPU+S4lvTd0j51GmBAdgQ==",
|
||||
"version": "11.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.0.tgz",
|
||||
"integrity": "sha512-bXfLy/mI8n4QICg+pWj1G8VduX5vC0SHRwFpiR5/Fxc8S2G906pSfkyMmHVsdJNQJQNh3LE67koad9GzEvkV6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browser-stdout": "^1.3.1",
|
||||
"chokidar": "^4.0.1",
|
||||
"debug": "^4.3.5",
|
||||
"diff": "^5.2.0",
|
||||
"diff": "^7.0.0",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"find-up": "^5.0.0",
|
||||
"glob": "^10.4.5",
|
||||
"he": "^1.2.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"log-symbols": "^4.1.0",
|
||||
"minimatch": "^5.1.6",
|
||||
"minimatch": "^9.0.5",
|
||||
"ms": "^2.1.3",
|
||||
"picocolors": "^1.1.1",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"strip-json-comments": "^3.1.1",
|
||||
"supports-color": "^8.1.1",
|
||||
"workerpool": "^6.5.1",
|
||||
"workerpool": "^9.2.0",
|
||||
"yargs": "^17.7.2",
|
||||
"yargs-parser": "^21.1.1",
|
||||
"yargs-unparser": "^2.0.0"
|
||||
@@ -7380,22 +7346,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/glob/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
@@ -7420,16 +7370,19 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mocha/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/path-scurry": {
|
||||
@@ -7855,17 +7808,14 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-imports": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz",
|
||||
"integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==",
|
||||
"node_modules/parse-imports-exports": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz",
|
||||
"integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-module-lexer": "^1.5.3",
|
||||
"slashes": "^3.0.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"parse-statements": "1.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-node-version": {
|
||||
@@ -7886,6 +7836,13 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-statements": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz",
|
||||
"integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
|
||||
@@ -8254,9 +8211,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.0.tgz",
|
||||
"integrity": "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -9186,12 +9143,6 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/slashes": {
|
||||
"version": "3.0.12",
|
||||
"resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz",
|
||||
"integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
@@ -9566,31 +9517,25 @@
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
|
||||
"integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
|
||||
"version": "0.11.8",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz",
|
||||
"integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.1.0",
|
||||
"tslib": "^2.6.2"
|
||||
"@pkgr/core": "^0.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/synckit/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
|
||||
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz",
|
||||
"integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10357,10 +10302,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/workerpool": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz",
|
||||
"integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==",
|
||||
"dev": true
|
||||
"version": "9.3.2",
|
||||
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.2.tgz",
|
||||
"integrity": "sha512-Xz4Nm9c+LiBHhDR5bDLnNzmj6+5F+cyEAWPMkbs2awq/dYazR/efelZzUAjB/y3kNHL+uzkHvxVVpaOfGCPV7A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
import {ConnectionType} from '../../build/src/core/connection_type.js';
|
||||
import {EventType} from '../../build/src/core/events/type.js';
|
||||
import * as eventUtils from '../../build/src/core/events/utils.js';
|
||||
import {IconType} from '../../build/src/core/icons/icon_types.js';
|
||||
import {EndRowInput} from '../../build/src/core/inputs/end_row_input.js';
|
||||
import {isCommentIcon} from '../../build/src/core/interfaces/i_comment_icon.js';
|
||||
import {Size} from '../../build/src/core/utils/size.js';
|
||||
import {assert} from '../../node_modules/chai/chai.js';
|
||||
import {createRenderedBlock} from './test_helpers/block_definitions.js';
|
||||
import {
|
||||
@@ -1426,9 +1429,9 @@ suite('Blocks', function () {
|
||||
});
|
||||
|
||||
suite('Constructing registered comment classes', function () {
|
||||
class MockComment extends MockIcon {
|
||||
class MockComment extends MockBubbleIcon {
|
||||
getType() {
|
||||
return Blockly.icons.IconType.COMMENT;
|
||||
return IconType.COMMENT;
|
||||
}
|
||||
|
||||
setText() {}
|
||||
@@ -1440,19 +1443,13 @@ suite('Blocks', function () {
|
||||
setBubbleSize() {}
|
||||
|
||||
getBubbleSize() {
|
||||
return Blockly.utils.Size(0, 0);
|
||||
return Size(0, 0);
|
||||
}
|
||||
|
||||
setBubbleLocation() {}
|
||||
|
||||
getBubbleLocation() {}
|
||||
|
||||
bubbleIsVisible() {
|
||||
return true;
|
||||
}
|
||||
|
||||
setBubbleVisible() {}
|
||||
|
||||
saveState() {
|
||||
return {};
|
||||
}
|
||||
@@ -1460,6 +1457,10 @@ suite('Blocks', function () {
|
||||
loadState() {}
|
||||
}
|
||||
|
||||
if (!isCommentIcon(new MockComment())) {
|
||||
throw new TypeError('MockComment not an ICommentIcon');
|
||||
}
|
||||
|
||||
setup(function () {
|
||||
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||
|
||||
|
||||
@@ -60,6 +60,33 @@ suite('Cursor', function () {
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'multi_statement_input',
|
||||
'message0': '%1 %2',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'FIRST',
|
||||
},
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'SECOND',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'type': 'simple_statement',
|
||||
'message0': '%1',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
},
|
||||
]);
|
||||
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||
this.cursor = this.workspace.getCursor();
|
||||
@@ -145,6 +172,112 @@ suite('Cursor', function () {
|
||||
assert.equal(curNode, this.blocks.D.nextConnection);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Multiple statement inputs', function () {
|
||||
setup(function () {
|
||||
sharedTestSetup.call(this);
|
||||
Blockly.defineBlocksWithJsonArray([
|
||||
{
|
||||
'type': 'multi_statement_input',
|
||||
'message0': '%1 %2',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'FIRST',
|
||||
},
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'SECOND',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'type': 'simple_statement',
|
||||
'message0': '%1',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
},
|
||||
]);
|
||||
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||
this.cursor = this.workspace.getCursor();
|
||||
|
||||
this.multiStatement1 = createRenderedBlock(
|
||||
this.workspace,
|
||||
'multi_statement_input',
|
||||
);
|
||||
this.multiStatement2 = createRenderedBlock(
|
||||
this.workspace,
|
||||
'multi_statement_input',
|
||||
);
|
||||
this.firstStatement = createRenderedBlock(
|
||||
this.workspace,
|
||||
'simple_statement',
|
||||
);
|
||||
this.secondStatement = createRenderedBlock(
|
||||
this.workspace,
|
||||
'simple_statement',
|
||||
);
|
||||
this.thirdStatement = createRenderedBlock(
|
||||
this.workspace,
|
||||
'simple_statement',
|
||||
);
|
||||
this.fourthStatement = createRenderedBlock(
|
||||
this.workspace,
|
||||
'simple_statement',
|
||||
);
|
||||
this.multiStatement1
|
||||
.getInput('FIRST')
|
||||
.connection.connect(this.firstStatement.previousConnection);
|
||||
this.firstStatement.nextConnection.connect(
|
||||
this.secondStatement.previousConnection,
|
||||
);
|
||||
this.multiStatement1
|
||||
.getInput('SECOND')
|
||||
.connection.connect(this.thirdStatement.previousConnection);
|
||||
this.multiStatement2
|
||||
.getInput('FIRST')
|
||||
.connection.connect(this.fourthStatement.previousConnection);
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
});
|
||||
|
||||
test('In - from field in nested statement block to next nested statement block', function () {
|
||||
this.cursor.setCurNode(this.secondStatement.getField('NAME'));
|
||||
this.cursor.in();
|
||||
const curNode = this.cursor.getCurNode();
|
||||
assert.equal(curNode, this.thirdStatement);
|
||||
});
|
||||
test('In - from field in nested statement block to next stack', function () {
|
||||
this.cursor.setCurNode(this.thirdStatement.getField('NAME'));
|
||||
this.cursor.in();
|
||||
const curNode = this.cursor.getCurNode();
|
||||
assert.equal(curNode, this.multiStatement2);
|
||||
});
|
||||
|
||||
test('Out - from nested statement block to last field of previous nested statement block', function () {
|
||||
this.cursor.setCurNode(this.thirdStatement);
|
||||
this.cursor.out();
|
||||
const curNode = this.cursor.getCurNode();
|
||||
assert.equal(curNode, this.secondStatement.getField('NAME'));
|
||||
});
|
||||
|
||||
test('Out - from root block to last field of last nested statement block in previous stack', function () {
|
||||
this.cursor.setCurNode(this.multiStatement2);
|
||||
this.cursor.out();
|
||||
const curNode = this.cursor.getCurNode();
|
||||
assert.equal(curNode, this.thirdStatement.getField('NAME'));
|
||||
});
|
||||
});
|
||||
|
||||
suite('Searching', function () {
|
||||
setup(function () {
|
||||
sharedTestSetup.call(this);
|
||||
|
||||
@@ -47,6 +47,16 @@ suite('Keyboard Shortcut Items', function () {
|
||||
.returns(block.nextConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workspace comment and set it as the focused node.
|
||||
* @param {Blockly.Workspace} workspace The workspace to create a new comment on.
|
||||
*/
|
||||
function setSelectedComment(workspace) {
|
||||
const comment = workspace.newComment();
|
||||
sinon.stub(Blockly.getFocusManager(), 'getFocusedNode').returns(comment);
|
||||
return comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a test for not running keyDown events when the workspace is in read only mode.
|
||||
* @param {Object} keyEvent Mocked key down event. Use createKeyDownEvent.
|
||||
@@ -173,12 +183,17 @@ suite('Keyboard Shortcut Items', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
// Do not copy a block if a workspace is in readonly mode.
|
||||
suite('Not called when readOnly is true', function () {
|
||||
// Allow copying a block if a workspace is in readonly mode.
|
||||
suite('Called when readOnly is true', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
runReadOnlyTest(keyEvent, testCaseName);
|
||||
test(testCaseName, function () {
|
||||
this.workspace.setIsReadOnly(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
});
|
||||
// Do not copy a block if a drag is in progress.
|
||||
@@ -236,6 +251,152 @@ suite('Keyboard Shortcut Items', function () {
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
// Copy a comment.
|
||||
test('Workspace comment', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
Blockly.getFocusManager().getFocusedNode.restore();
|
||||
this.comment = setSelectedComment(this.workspace);
|
||||
this.copySpy = sinon.spy(this.comment, 'toCopyData');
|
||||
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Cut', function () {
|
||||
setup(function () {
|
||||
this.block = setSelectedBlock(this.workspace);
|
||||
this.copySpy = sinon.spy(this.block, 'toCopyData');
|
||||
this.disposeSpy = sinon.spy(this.block, 'dispose');
|
||||
this.hideChaffSpy = sinon.spy(
|
||||
Blockly.WorkspaceSvg.prototype,
|
||||
'hideChaff',
|
||||
);
|
||||
});
|
||||
const testCases = [
|
||||
[
|
||||
'Control X',
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.X, [
|
||||
Blockly.utils.KeyCodes.CTRL,
|
||||
]),
|
||||
],
|
||||
[
|
||||
'Meta X',
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.X, [
|
||||
Blockly.utils.KeyCodes.META,
|
||||
]),
|
||||
],
|
||||
];
|
||||
// Cut a block.
|
||||
suite('Simple', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.disposeSpy);
|
||||
sinon.assert.calledOnce(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
});
|
||||
// Do not cut a block if a workspace is in readonly mode.
|
||||
suite('Not called when readOnly is true', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
this.workspace.setIsReadOnly(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
});
|
||||
// Do not cut a block if a drag is in progress.
|
||||
suite('Drag in progress', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
sinon.stub(this.workspace, 'isDragging').returns(true);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
});
|
||||
// Do not cut a block if is is not deletable.
|
||||
suite('Block is not deletable', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
sinon
|
||||
.stub(Blockly.common.getSelected(), 'isOwnDeletable')
|
||||
.returns(false);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
});
|
||||
// Do not cut a block if it is not movable.
|
||||
suite('Block is not movable', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
sinon
|
||||
.stub(Blockly.common.getSelected(), 'isOwnMovable')
|
||||
.returns(false);
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
});
|
||||
});
|
||||
test('Not called when connection is focused', function () {
|
||||
// Restore the stub behavior called during setup
|
||||
Blockly.getFocusManager().getFocusedNode.restore();
|
||||
|
||||
setSelectedConnection(this.workspace);
|
||||
const event = createKeyDownEvent(Blockly.utils.KeyCodes.C, [
|
||||
Blockly.utils.KeyCodes.CTRL,
|
||||
]);
|
||||
this.injectionDiv.dispatchEvent(event);
|
||||
sinon.assert.notCalled(this.copySpy);
|
||||
sinon.assert.notCalled(this.disposeSpy);
|
||||
sinon.assert.notCalled(this.hideChaffSpy);
|
||||
});
|
||||
|
||||
// Cut a comment.
|
||||
suite('Workspace comment', function () {
|
||||
testCases.forEach(function (testCase) {
|
||||
const testCaseName = testCase[0];
|
||||
const keyEvent = testCase[1];
|
||||
test(testCaseName, function () {
|
||||
Blockly.getFocusManager().getFocusedNode.restore();
|
||||
this.comment = setSelectedComment(this.workspace);
|
||||
this.copySpy = sinon.spy(this.comment, 'toCopyData');
|
||||
this.disposeSpy = sinon.spy(this.comment, 'dispose');
|
||||
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
sinon.assert.calledOnce(this.copySpy);
|
||||
sinon.assert.calledOnce(this.disposeSpy);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Undo', function () {
|
||||
|
||||
@@ -4,7 +4,24 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export class MockIcon {
|
||||
import {isFocusableNode} from '../../../build/src/core/interfaces/i_focusable_node.js';
|
||||
import {hasBubble} from '../../../build/src/core/interfaces/i_has_bubble.js';
|
||||
import {isIcon} from '../../../build/src/core/interfaces/i_icon.js';
|
||||
import {isSerializable} from '../../../build/src/core/interfaces/i_serializable.js';
|
||||
|
||||
export class MockFocusable {
|
||||
getFocusableElement() {}
|
||||
getFocusableTree() {}
|
||||
onNodeFocus() {}
|
||||
onNodeBlur() {}
|
||||
canBeFocused() {}
|
||||
}
|
||||
|
||||
if (!isFocusableNode(new MockFocusable())) {
|
||||
throw new TypeError('MockFocusable not an IFocuableNode');
|
||||
}
|
||||
|
||||
export class MockIcon extends MockFocusable {
|
||||
getType() {
|
||||
return new Blockly.icons.IconType('mock icon');
|
||||
}
|
||||
@@ -52,6 +69,10 @@ export class MockIcon {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isIcon(new MockIcon())) {
|
||||
throw new TypeError('MockIcon not an IIcon');
|
||||
}
|
||||
|
||||
export class MockSerializableIcon extends MockIcon {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -75,6 +96,10 @@ export class MockSerializableIcon extends MockIcon {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSerializable(new MockSerializableIcon())) {
|
||||
throw new TypeError('MockSerializableIcon not an ISerializable');
|
||||
}
|
||||
|
||||
export class MockBubbleIcon extends MockIcon {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -94,4 +119,12 @@ export class MockBubbleIcon extends MockIcon {
|
||||
setBubbleVisible(visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
getBubble() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasBubble(new MockBubbleIcon())) {
|
||||
throw new TypeError('MockBubbleIcon not an IHasBubble');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user