chore: Merge branch develop into no-checkin-built

Files that have had manual merge conflict resolution performed:

- gulpfile.js

Files where a merge conflic was resolved by deleting the file, because
it had previously been deleted in no-checkin-built:

- *_compresed.js*
- msg/js/*.js
- tests/run_all_tests.sh

Changes made (in no-checkin-built) to the following files that
have been deleted or obsoleted will need to be dealt with
manually; this will be done in a separate commit for clarity:

- tests/run_all_tests.sh
- tests/scripts/check_metadata.sh
- tests/scripts/run_generators.sh
This commit is contained in:
Christopher Allen
2022-10-28 19:44:39 +01:00
407 changed files with 6864 additions and 5731 deletions

View File

@@ -160,7 +160,6 @@
"jsdoc/check-param-names": ["off", {"checkDestructured": false}],
// Allow any text in the license tag. Other checks are not relevant.
"jsdoc/check-values": ["off"]
}
}]
}

View File

@@ -29,9 +29,9 @@ All submissions, including submissions by project members, require review. We
use Github pull requests for this purpose.
### Browser compatibility
We care strongly about making Blockly work on all browsers. As of 2022 we
We care strongly about making Blockly work on all browsers. As of 2022 we
support Edge, Chrome, Safari, and Firefox. We will not accept changes that only
work on a subset of those browsers. You can check [caniuse.com](https://caniuse.com/)
work on a subset of those browsers. You can check [caniuse.com](https://caniuse.com/)
for compatibility information.
### The small print

View File

@@ -35,11 +35,11 @@
### Test Coverage
<!-- TODO: Please show how you have added tests to cover your changes,
- or tell us how you tested it. If your change includes
- browser-specific behaviour, include information about the
- browser and device that you used for testing.
-->
<!-- TODO: Please create unit tests, and explain here how they cover
your changes, or tell us how you tested it manually. If
your changes include browser-specific behaviour, include
information about the browser and device that you used for
testing. -->
### Documentation

View File

@@ -22,7 +22,7 @@ updates:
- "PR: chore"
- "PR: dependencies"
- package-ecosystem: "github-actions" # See documentation for possible values
directory: "/"
directory: "/"
target-branch: "develop"
schedule:
interval: "weekly"

View File

@@ -1,7 +0,0 @@
primaryBranch: develop
releaseType: node
packageName: blockly
manifest: true
manifestConfig: release-please-config.json
manifestFile: .release-please-manifest.json
handleGHRelease: true

View File

@@ -42,7 +42,7 @@ jobs:
path: _deploy/
- name: Deploy to App Engine
uses: google-github-actions/deploy-appengine@v0.8.0
uses: google-github-actions/deploy-appengine@v0.8.2
# For parameters see:
# https://github.com/google-github-actions/deploy-appengine#inputs
with:

View File

@@ -0,0 +1,12 @@
on:
pull_request_target:
types: [ opened, edited ]
name: conventional-release-labels
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: bcoe/conventional-release-labels@v1
with:
type_labels: '{"feat": "PR: feature", "fix": "PR: fix", "breaking": "breaking change", "chore": "PR: chore", "docs": "PR: docs", "refactor": "PR: refactor"}'
ignored_types: '[]'

View File

@@ -5,14 +5,14 @@ name: Tag module cleanup
# Trigger on pull requests against goog_module branch only
# Uses pull_request_target to get write permissions so that it can write labels.
on:
on:
pull_request_target:
branches:
- goog_module
jobs:
tag-module-cleanup:
# Add the type: cleanup label
runs-on: ubuntu-latest
steps:

View File

@@ -13,7 +13,7 @@ jobs:
permissions:
contents: write # for peter-evans/create-pull-request to create branch
pull-requests: write # for peter-evans/create-pull-request to create a PR
runs-on: ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check Out Blockly
@@ -36,7 +36,7 @@ jobs:
run: source ./tests/scripts/update_metadata.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@18f90432bedd2afd6a825469ffd38aa24712a91d
uses: peter-evans/create-pull-request@b4d51739f96fca8047ad065eccef63442d8e99f7
with:
commit-message: Update build artifact sizes in check_metadata.sh
delete-branch: true

View File

@@ -1,3 +0,0 @@
{
".": "8.0.0"
}

View File

@@ -379,11 +379,6 @@
},
"tsdoc-escape-right-brace": {
"logLevel": "none"
},
// Needs to be fixed
"tsdoc-missing-deprecation-message": {
"logLevel": "none"
}
}
}

View File

@@ -133,7 +133,7 @@ blocks['lists_create_with'] = {
this.itemCount_ = 3;
this.updateShape_();
this.setOutput(true, 'Array');
this.setMutator(new Mutator(['lists_create_with_item']));
this.setMutator(new Mutator(['lists_create_with_item'], this));
this.setTooltip(Msg['LISTS_CREATE_WITH_TOOLTIP']);
},
/**

View File

@@ -150,8 +150,7 @@ const PROCEDURE_DEF_COMMON = {
this.argumentVarModels_.push(variable);
} else {
console.log(
'Failed to create a variable with name ' + varName +
', ignoring.');
`Failed to create a variable named "${varName}", ignoring.`);
}
}
}
@@ -461,7 +460,7 @@ blocks['procedures_defnoreturn'] = {
.appendField(Msg['PROCEDURES_DEFNORETURN_TITLE'])
.appendField(nameField, 'NAME')
.appendField('', 'PARAMS');
this.setMutator(new Mutator(['procedures_mutatorarg']));
this.setMutator(new Mutator(['procedures_mutatorarg'], this));
if ((this.workspace.options.comments ||
(this.workspace.options.parentWorkspace &&
this.workspace.options.parentWorkspace.options.comments)) &&
@@ -507,7 +506,7 @@ blocks['procedures_defreturn'] = {
this.appendValueInput('RETURN')
.setAlign(Align.RIGHT)
.appendField(Msg['PROCEDURES_DEFRETURN_RETURN']);
this.setMutator(new Mutator(['procedures_mutatorarg']));
this.setMutator(new Mutator(['procedures_mutatorarg'], this));
if ((this.workspace.options.comments ||
(this.workspace.options.parentWorkspace &&
this.workspace.options.parentWorkspace.options.comments)) &&

View File

@@ -864,7 +864,7 @@ const TEXT_JOIN_EXTENSION = function() {
this.itemCount_ = 2;
this.updateShape_();
// Configure the mutator UI.
this.setMutator(new Mutator(['text_create_join_item']));
this.setMutator(new Mutator(['text_create_join_item'], this));
};
// Update the tooltip of 'text_append' block to reference the variable.

View File

@@ -23,7 +23,7 @@ var goog = goog || {};
/**
* Reference to the global object. This is provided as 'root' by the
* UMD wrapper, but prefer globalThis if it is defined.
*
*
* https://www.ecma-international.org/ecma-262/9.0/index.html#sec-global-object
*
* @const

View File

@@ -180,6 +180,11 @@ export class Block implements IASTNodeLocation, IDeletable {
protected collapsed_ = false;
protected outputShape_: number|null = null;
/**
* Is the current block currently in the process of being disposed?
*/
private disposing = false;
/**
* A string representing the comment attached to this block.
*
@@ -316,7 +321,7 @@ export class Block implements IASTNodeLocation, IDeletable {
* @suppress {checkTypes}
*/
dispose(healStack: boolean) {
if (this.disposed) {
if (this.isDeadOrDying()) {
return;
}
@@ -338,6 +343,7 @@ export class Block implements IASTNodeLocation, IDeletable {
this.workspace.removeTypedBlock(this);
// Remove from block database.
this.workspace.removeBlockById(this.id);
this.disposing = true;
// First, dispose of all my children.
for (let i = this.childBlocks_.length - 1; i >= 0; i--) {
@@ -360,6 +366,16 @@ export class Block implements IASTNodeLocation, IDeletable {
}
}
/**
* Returns true if the block is either in the process of being disposed, or
* is disposed.
*
* @internal
*/
isDeadOrDying(): boolean {
return this.disposing || this.disposed;
}
/**
* Call initModel on all fields on the block.
* May be called more than once.
@@ -772,7 +788,7 @@ export class Block implements IASTNodeLocation, IDeletable {
* @returns True if deletable.
*/
isDeletable(): boolean {
return this.deletable_ && !this.isShadow_ && !this.disposed &&
return this.deletable_ && !this.isShadow_ && !this.isDeadOrDying() &&
!this.workspace.options.readOnly;
}
@@ -791,7 +807,7 @@ export class Block implements IASTNodeLocation, IDeletable {
* @returns True if movable.
*/
isMovable(): boolean {
return this.movable_ && !this.isShadow_ && !this.disposed &&
return this.movable_ && !this.isShadow_ && !this.isDeadOrDying() &&
!this.workspace.options.readOnly;
}
@@ -865,7 +881,8 @@ export class Block implements IASTNodeLocation, IDeletable {
* @returns True if editable.
*/
isEditable(): boolean {
return this.editable_ && !this.disposed && !this.workspace.options.readOnly;
return this.editable_ && !this.isDeadOrDying() &&
!this.workspace.options.readOnly;
}
/**

View File

@@ -188,7 +188,11 @@ function disconnectUiStep(group: SVGElement, magnitude: number, start: Date) {
skew = `skewX(${val})`;
disconnectPid = setTimeout(disconnectUiStep, 10, group, magnitude, start);
}
group.setAttribute('transform', skew);
(group as AnyDuringMigration).skew_ = skew;
group.setAttribute(
'transform',
(group as AnyDuringMigration).translate_ +
(group as AnyDuringMigration).skew_);
}
/**
@@ -202,7 +206,9 @@ export function disconnectUiStop() {
if (disconnectPid) {
clearTimeout(disconnectPid);
}
disconnectGroup.setAttribute('transform', '');
const group = disconnectGroup;
(group as AnyDuringMigration).skew_ = '';
group.setAttribute('transform', (group as AnyDuringMigration).translate_);
disconnectGroup = null;
}
}

View File

@@ -31,39 +31,40 @@ import * as svgMath from './utils/svg_math.js';
* @alias Blockly.BlockDragSurfaceSvg
*/
export class BlockDragSurfaceSvg {
/** The SVG drag surface. Set once by BlockDragSurfaceSvg.createDom. */
private svg_: SVGElement;
/**
* The root element of the drag surface.
*/
private svg: SVGElement;
/**
* This is where blocks live while they are being dragged if the drag
* surface is enabled.
*/
private dragGroup_: SVGElement;
private dragGroup: SVGElement;
/**
* Cached value for the scale of the drag surface.
* Used to set/get the correct translation during and after a drag.
*/
private scale_ = 1;
private scale = 1;
/**
* Cached value for the translation of the drag surface.
* This translation is in pixel units, because the scale is applied to the
* drag group rather than the top-level SVG.
*/
private surfaceXY_: Coordinate = new Coordinate(0, 0);
private readonly childSurfaceXY_: Coordinate;
private surfaceXY = new Coordinate(0, 0);
/**
* Cached value for the translation of the child drag surface in pixel
* units. Since the child drag surface tracks the translation of the
* workspace this is ultimately the translation of the workspace.
*/
private readonly childSurfaceXY = new Coordinate(0, 0);
/** @param container Containing element. */
constructor(private readonly container: Element) {
/**
* Cached value for the translation of the child drag surface in pixel
* units. Since the child drag surface tracks the translation of the
* workspace this is ultimately the translation of the workspace.
*/
this.childSurfaceXY_ = new Coordinate(0, 0);
this.svg_ = dom.createSvgElement(
this.svg = dom.createSvgElement(
Svg.SVG, {
'xmlns': dom.SVG_NS,
'xmlns:html': dom.HTML_NS,
@@ -72,10 +73,15 @@ export class BlockDragSurfaceSvg {
'class': 'blocklyBlockDragSurface',
},
this.container);
this.dragGroup_ = dom.createSvgElement(Svg.G, {}, this.svg_ as SVGElement);
this.dragGroup = dom.createSvgElement(Svg.G, {}, this.svg);
}
/** Create the drag surface and inject it into the container. */
/**
* Create the drag surface and inject it into the container.
*
* @deprecated The DOM is automatically created by the constructor.
*/
createDom() {
// No alternative provided, because now the dom is just automatically
// created in the constructor now.
@@ -89,13 +95,13 @@ export class BlockDragSurfaceSvg {
* @param blocks Block or group of blocks to place on the drag surface.
*/
setBlocksAndShow(blocks: SVGElement) {
if (this.dragGroup_.childNodes.length) {
if (this.dragGroup.childNodes.length) {
throw Error('Already dragging a block.');
}
// appendChild removes the blocks from the previous parent
this.dragGroup_.appendChild(blocks);
this.svg_.style.display = 'block';
this.surfaceXY_ = new Coordinate(0, 0);
this.dragGroup.appendChild(blocks);
this.svg.style.display = 'block';
this.surfaceXY = new Coordinate(0, 0);
}
/**
@@ -107,13 +113,13 @@ export class BlockDragSurfaceSvg {
* @param scale Scale of the group.
*/
translateAndScaleGroup(x: number, y: number, scale: number) {
this.scale_ = scale;
this.scale = scale;
// Make sure the svg exists on a pixel boundary so that it is not fuzzy.
const roundX = Math.round(x);
const roundY = Math.round(y);
this.childSurfaceXY_.x = roundX;
this.childSurfaceXY_.y = roundY;
this.dragGroup_!.setAttribute(
this.childSurfaceXY.x = roundX;
this.childSurfaceXY.y = roundY;
this.dragGroup.setAttribute(
'transform',
'translate(' + roundX + ',' + roundY + ') scale(' + scale + ')');
}
@@ -124,13 +130,11 @@ export class BlockDragSurfaceSvg {
* @internal
*/
translateSurfaceInternal_() {
let x = this.surfaceXY_!.x;
let y = this.surfaceXY_!.y;
// Make sure the svg exists on a pixel boundary so that it is not fuzzy.
x = Math.round(x);
y = Math.round(y);
this.svg_.style.display = 'block';
dom.setCssTransform(this.svg_, 'translate3d(' + x + 'px, ' + y + 'px, 0)');
const x = Math.round(this.surfaceXY.x);
const y = Math.round(this.surfaceXY.y);
this.svg.style.display = 'block';
dom.setCssTransform(this.svg, 'translate3d(' + x + 'px, ' + y + 'px, 0)');
}
/**
@@ -140,9 +144,9 @@ export class BlockDragSurfaceSvg {
* @param deltaY Vertical offset in pixel units.
*/
translateBy(deltaX: number, deltaY: number) {
const x = this.surfaceXY_.x + deltaX;
const y = this.surfaceXY_.y + deltaY;
this.surfaceXY_ = new Coordinate(x, y);
const x = this.surfaceXY.x + deltaX;
const y = this.surfaceXY.y + deltaY;
this.surfaceXY = new Coordinate(x, y);
this.translateSurfaceInternal_();
}
@@ -156,7 +160,7 @@ export class BlockDragSurfaceSvg {
* @param y Y translation for the entire surface.
*/
translateSurface(x: number, y: number) {
this.surfaceXY_ = new Coordinate(x * this.scale_, y * this.scale_);
this.surfaceXY = new Coordinate(x * this.scale, y * this.scale);
this.translateSurfaceInternal_();
}
@@ -167,8 +171,8 @@ export class BlockDragSurfaceSvg {
* @returns Current translation of the surface.
*/
getSurfaceTranslation(): Coordinate {
const xy = svgMath.getRelativeXY(this.svg_ as SVGElement);
return new Coordinate(xy.x / this.scale_, xy.y / this.scale_);
const xy = svgMath.getRelativeXY(this.svg);
return new Coordinate(xy.x / this.scale, xy.y / this.scale);
}
/**
@@ -177,8 +181,8 @@ export class BlockDragSurfaceSvg {
*
* @returns Drag surface group element.
*/
getGroup(): SVGElement|null {
return this.dragGroup_;
getGroup(): SVGElement {
return this.dragGroup;
}
/**
@@ -186,8 +190,8 @@ export class BlockDragSurfaceSvg {
*
* @returns The SVG drag surface.
*/
getSvgRoot(): SVGElement|null {
return this.svg_;
getSvgRoot(): SVGElement {
return this.svg;
}
/**
@@ -197,7 +201,7 @@ export class BlockDragSurfaceSvg {
* @returns Drag surface block DOM element, or null if no blocks exist.
*/
getCurrentBlock(): Element|null {
return this.dragGroup_.firstChild as Element;
return this.dragGroup.firstChild as Element;
}
/**
@@ -209,7 +213,7 @@ export class BlockDragSurfaceSvg {
*/
getWsTranslation(): Coordinate {
// Returning a copy so the coordinate can not be changed outside this class.
return this.childSurfaceXY_.clone();
return this.childSurfaceXY.clone();
}
/**
@@ -226,16 +230,16 @@ export class BlockDragSurfaceSvg {
const currentBlockElement = this.getCurrentBlock();
if (currentBlockElement) {
if (opt_newSurface) {
// appendChild removes the node from this.dragGroup_
// appendChild removes the node from this.dragGroup
opt_newSurface.appendChild(currentBlockElement);
} else {
this.dragGroup_.removeChild(currentBlockElement);
this.dragGroup.removeChild(currentBlockElement);
}
}
this.svg_.style.display = 'none';
if (this.dragGroup_.childNodes.length) {
this.svg.style.display = 'none';
if (this.dragGroup.childNodes.length) {
throw Error('Drag group was not cleared.');
}
this.surfaceXY_ = new Coordinate(0, 0);
this.surfaceXY = new Coordinate(0, 0);
}
}

View File

@@ -241,7 +241,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*
* @returns #RRGGBB string.
*/
getColourSecondary(): string|null {
getColourSecondary(): string|undefined {
return this.style.colourSecondary;
}
@@ -250,7 +250,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
*
* @returns #RRGGBB string.
*/
getColourTertiary(): string|null {
getColourTertiary(): string|undefined {
return this.style.colourTertiary;
}
@@ -525,7 +525,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
/** Snap this block to the nearest grid point. */
snapToGrid() {
if (this.disposed) {
if (this.isDeadOrDying()) {
return; // Deleted block.
}
if (this.workspace.isDragging()) {
@@ -778,10 +778,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
(group as AnyDuringMigration).translate_ = '';
(group as AnyDuringMigration).skew_ = '';
common.draggingConnections.push(...this.getConnections_(true));
this.svgGroup_.classList.add('blocklyDragging');
dom.addClass(this.svgGroup_, 'blocklyDragging');
} else {
common.draggingConnections.length = 0;
this.svgGroup_.classList.remove('blocklyDragging');
dom.removeClass(this.svgGroup_, 'blocklyDragging');
}
// Recurse through all blocks attached under this one.
for (let i = 0; i < this.childBlocks_.length; i++) {
@@ -861,8 +861,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
* @suppress {checkTypes}
*/
override dispose(healStack?: boolean, animate?: boolean) {
if (this.disposed) {
// The block has already been deleted.
if (this.isDeadOrDying()) {
return;
}
Tooltip.dispose();
@@ -981,8 +980,8 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
}
/**
* Updates the color of the block (and children) to match the current disabled
* state.
* Updates the colour of the block (and children) to match the current
* disabled state.
*
* @internal
*/
@@ -1067,13 +1066,12 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
if (this.workspace.isDragging()) {
// Don't change the warning text during a drag.
// Wait until the drag finishes.
this.warningTextDb.set(
id, setTimeout(() => {
if (!this.disposed) { // Check block wasn't deleted.
this.warningTextDb.delete(id);
this.setWarningText(text, id);
}
}, 100));
this.warningTextDb.set(id, setTimeout(() => {
if (!this.isDeadOrDying()) {
this.warningTextDb.delete(id);
this.setWarningText(text, id);
}
}, 100));
return;
}
if (this.isInFlyout) {
@@ -1537,45 +1535,46 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
}
/**
* Bump unconnected blocks out of alignment. Two blocks which aren't actually
* connected should not coincidentally line up on screen.
* Bumps unconnected blocks out of alignment.
*
* Two blocks which aren't actually connected should not coincidentally line
* up on screen, because that creates confusion for end-users.
*/
override bumpNeighbours() {
if (this.disposed) {
return; // Deleted block.
}
if (this.workspace.isDragging()) {
return; // Don't bump blocks during a drag.
}
const rootBlock = this.getRootBlock();
if (rootBlock.isInFlyout) {
this.getRootBlock().bumpNeighboursInternal();
}
/**
* Bumps unconnected blocks out of alignment.
*/
private bumpNeighboursInternal() {
const root = this.getRootBlock();
if (this.isDeadOrDying() || this.workspace.isDragging() ||
root.isInFlyout) {
return;
}
// Don't move blocks around in a flyout.
// Loop through every connection on this block.
const myConnections = this.getConnections_(false);
for (let i = 0, connection; connection = myConnections[i]; i++) {
const renderedConn = (connection);
// Spider down from this block bumping all sub-blocks.
if (renderedConn.isConnected() && renderedConn.isSuperior()) {
renderedConn.targetBlock()!.bumpNeighbours();
function neighbourIsInStack(neighbour: RenderedConnection) {
return neighbour.getSourceBlock().getRootBlock() === root;
}
for (const conn of this.getConnections_(false)) {
if (conn.isSuperior()) {
// Recurse down the block stack.
conn.targetBlock()?.bumpNeighboursInternal();
}
const neighbours = connection.neighbours(config.snapRadius);
for (let j = 0, otherConnection; otherConnection = neighbours[j]; j++) {
const renderedOther = otherConnection as RenderedConnection;
// If both connections are connected, that's probably fine. But if
// either one of them is unconnected, then there could be confusion.
if (!renderedConn.isConnected() || !renderedOther.isConnected()) {
// Only bump blocks if they are from different tree structures.
if (renderedOther.getSourceBlock().getRootBlock() !== rootBlock) {
// Always bump the inferior block.
if (renderedConn.isSuperior()) {
renderedOther.bumpAwayFrom(renderedConn);
} else {
renderedConn.bumpAwayFrom(renderedOther);
}
}
for (const neighbour of conn.neighbours(config.snapRadius)) {
// Don't bump away from things that are in our stack.
if (neighbourIsInStack(neighbour)) continue;
// If both connections are connected, that's fine.
if (conn.isConnected() && neighbour.isConnected()) continue;
// Always bump the inferior connection.
if (conn.isSuperior()) {
neighbour.bumpAwayFrom(conn);
} else {
conn.bumpAwayFrom(neighbour);
}
}
}

View File

@@ -57,7 +57,7 @@ import {Field} from './field.js';
import {FieldAngle} from './field_angle.js';
import {FieldCheckbox} from './field_checkbox.js';
import {FieldColour} from './field_colour.js';
import {FieldDropdown} from './field_dropdown.js';
import {FieldDropdown, MenuGenerator, MenuGeneratorFunction, MenuOption} from './field_dropdown.js';
import {FieldImage} from './field_image.js';
import {FieldLabel} from './field_label.js';
import {FieldLabelSerializable} from './field_label_serializable.js';
@@ -71,7 +71,7 @@ import {FlyoutButton} from './flyout_button.js';
import {HorizontalFlyout} from './flyout_horizontal.js';
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
import {VerticalFlyout} from './flyout_vertical.js';
import {Generator} from './generator.js';
import {CodeGenerator} from './generator.js';
import {Gesture} from './gesture.js';
import {Grid} from './grid.js';
import {Icon} from './icon.js';
@@ -104,7 +104,6 @@ import {IRegistrable} from './interfaces/i_registrable.js';
import {IRegistrableField} from './interfaces/i_registrable_field.js';
import {ISelectable} from './interfaces/i_selectable.js';
import {ISelectableToolboxItem} from './interfaces/i_selectable_toolbox_item.js';
import {ISerializer as SerializerInterface} from './interfaces/i_serializer.js';
import {IStyleable} from './interfaces/i_styleable.js';
import {IToolbox} from './interfaces/i_toolbox.js';
import {IToolboxItem} from './interfaces/i_toolbox_item.js';
@@ -118,7 +117,7 @@ import {MarkerManager} from './marker_manager.js';
import {Menu} from './menu.js';
import {MenuItem} from './menuitem.js';
import {MetricsManager} from './metrics_manager.js';
import {Msg} from './msg.js';
import {Msg, setLocale} from './msg.js';
import {Mutator} from './mutator.js';
import {Names} from './names.js';
import {Options} from './options.js';
@@ -134,12 +133,7 @@ import * as thrasos from './renderers/thrasos/thrasos.js';
import * as zelos from './renderers/zelos/zelos.js';
import {Scrollbar} from './scrollbar.js';
import {ScrollbarPair} from './scrollbar_pair.js';
import * as serializationBlocks from './serialization/blocks.js';
import * as serializationExceptions from './serialization/exceptions.js';
import * as serializationPriorities from './serialization/priorities.js';
import * as serializationRegistry from './serialization/registry.js';
import * as serializationVariables from './serialization/variables.js';
import * as serializationWorkspaces from './serialization/workspaces.js';
import * as serialization from './serialization.js';
import * as ShortcutItems from './shortcut_items.js';
import {ShortcutRegistry} from './shortcut_registry.js';
import {Theme} from './theme.js';
@@ -157,7 +151,6 @@ import {Trashcan} from './trashcan.js';
import * as utils from './utils.js';
import * as colour from './utils/colour.js';
import * as deprecation from './utils/deprecation.js';
import * as svgMath from './utils/svg_math.js';
import * as toolbox from './utils/toolbox.js';
import {VariableMap} from './variable_map.js';
import {VariableModel} from './variable_model.js';
@@ -345,23 +338,12 @@ export const defineBlocksWithJsonArray = common.defineBlocksWithJsonArray;
*/
export const setParentContainer = common.setParentContainer;
/**
* Returns the dimensions of the specified SVG image.
*
* @param svg SVG image.
* @returns Contains width and height properties.
* @deprecated Use workspace.setCachedParentSvgSize. (2021 March 5)
* @see Blockly.WorkspaceSvg.setCachedParentSvgSize
* @alias Blockly.svgSize
*/
export const svgSize = svgMath.svgSize;
/**
* Size the workspace when the contents change. This also updates
* scrollbars accordingly.
*
* @param workspace The workspace to resize.
* @deprecated Use workspace.resizeContents. (2021 December)
* @deprecated Use **workspace.resizeContents** instead.
* @see Blockly.WorkspaceSvg.resizeContents
* @alias Blockly.resizeSvgContents
*/
@@ -377,7 +359,7 @@ export const resizeSvgContents = resizeSvgContentsLocal;
* Copy a block or workspace comment onto the local clipboard.
*
* @param toCopy Block or Workspace Comment to be copied.
* @deprecated Use Blockly.clipboard.copy(). (2021 December)
* @deprecated Use **Blockly.clipboard.copy** instead.
* @see Blockly.clipboard.copy
* @alias Blockly.copy
*/
@@ -392,7 +374,7 @@ export function copy(toCopy: ICopyable) {
* Paste a block or workspace comment on to the main workspace.
*
* @returns True if the paste was successful, false otherwise.
* @deprecated Use Blockly.clipboard.paste(). (2021 December)
* @deprecated Use **Blockly.clipboard.paste** instead.
* @see Blockly.clipboard.paste
* @alias Blockly.paste
*/
@@ -407,7 +389,7 @@ export function paste(): boolean {
* Duplicate this block and its children, or a workspace comment.
*
* @param toDuplicate Block or Workspace Comment to be copied.
* @deprecated Use Blockly.clipboard.duplicate(). (2021 December)
* @deprecated Use **Blockly.clipboard.duplicate** instead.
* @see Blockly.clipboard.duplicate
* @alias Blockly.duplicate
*/
@@ -423,7 +405,7 @@ export function duplicate(toDuplicate: ICopyable) {
*
* @param str Input string.
* @returns True if number, false otherwise.
* @deprecated Use Blockly.utils.string.isNumber(str). (2021 December)
* @deprecated Use **Blockly.utils.string.isNumber** instead.
* @see Blockly.utils.string.isNumber
* @alias Blockly.isNumber
*/
@@ -439,7 +421,7 @@ export function isNumber(str: string): boolean {
*
* @param hue Hue on a colour wheel (0-360).
* @returns RGB code, e.g. '#5ba65b'.
* @deprecated Use Blockly.utils.colour.hueToHex(). (2021 December)
* @deprecated Use **Blockly.utils.colour.hueToHex** instead.
* @see Blockly.utils.colour.hueToHex
* @alias Blockly.hueToHex
*/
@@ -461,7 +443,7 @@ export function hueToHex(hue: number): string {
* @param thisObject The value of 'this' in the function.
* @param func Function to call when event is triggered.
* @returns Opaque data that can be passed to unbindEvent_.
* @deprecated Use Blockly.browserEvents.bind(). (December 2021)
* @deprecated Use **Blockly.browserEvents.bind** instead.
* @see Blockly.browserEvents.bind
* @alias Blockly.bindEvent_
*/
@@ -480,7 +462,7 @@ export function bindEvent_(
* @param bindData Opaque data from bindEvent_.
* This list is emptied during the course of calling this function.
* @returns The function call.
* @deprecated Use Blockly.browserEvents.unbind(). (December 2021)
* @deprecated Use **Blockly.browserEvents.unbind** instead.
* @see browserEvents.unbind
* @alias Blockly.unbindEvent_
*/
@@ -508,7 +490,7 @@ export function unbindEvent_(bindData: browserEvents.Data): Function {
* the default handler. False by default. If opt_noPreventDefault is
* provided, opt_noCaptureIdentifier must also be provided.
* @returns Opaque data that can be passed to unbindEvent_.
* @deprecated Use Blockly.browserEvents.conditionalBind(). (December 2021)
* @deprecated Use **Blockly.browserEvents.conditionalBind** instead.
* @see browserEvents.conditionalBind
* @alias Blockly.bindEventWithChecks_
*/
@@ -560,6 +542,7 @@ export const VARIABLE_DYNAMIC_CATEGORY_NAME: string =
*/
export const PROCEDURE_CATEGORY_NAME: string = Procedures.CATEGORY_NAME;
// Context for why we need to monkey-patch in these functions (internal):
// https://docs.google.com/document/d/1MbO0LEA-pAyx1ErGLJnyUqTLrcYTo-5zga9qplnxeXo/edit?usp=sharing&resourcekey=0-5h_32-i-dHwHjf_9KYEVKg
@@ -584,12 +567,12 @@ WorkspaceCommentSvg.prototype.showContextMenu =
return;
}
const menuOptions = [];
if (this.isDeletable() && this.isMovable()) {
menuOptions.push(ContextMenu.commentDuplicateOption(this));
menuOptions.push(ContextMenu.commentDeleteOption(this));
}
ContextMenu.show(e, menuOptions, this.RTL);
};
@@ -619,6 +602,7 @@ export {Css};
export {Events};
export {Extensions};
export {Procedures};
export {Procedures as procedures};
export {ShortcutItems};
export {Themes};
export {Tooltip};
@@ -668,7 +652,7 @@ export {Field};
export {FieldAngle};
export {FieldCheckbox};
export {FieldColour};
export {FieldDropdown};
export {FieldDropdown, MenuGenerator, MenuGeneratorFunction, MenuOption};
export {FieldImage};
export {FieldLabel};
export {FieldLabelSerializable};
@@ -679,7 +663,8 @@ export {FieldVariable};
export {Flyout};
export {FlyoutButton};
export {FlyoutMetricsManager};
export {Generator};
export {CodeGenerator};
export {CodeGenerator as Generator}; // Deprecated name, October 2022.
export {Gesture};
export {Grid};
export {HorizontalFlyout};
@@ -720,7 +705,7 @@ export {Menu};
export {MenuItem};
export {MetricsManager};
export {Mutator};
export {Msg};
export {Msg, setLocale};
export {Names};
export {Options};
export {RenderedConnection};
@@ -753,12 +738,4 @@ export {config};
export const connectionTypes = ConnectionType;
export {inject};
export {inputTypes};
export namespace serialization {
export const blocks = serializationBlocks;
export const exceptions = serializationExceptions;
export const priorities = serializationPriorities;
export const registry = serializationRegistry;
export const variables = serializationVariables;
export const workspaces = serializationWorkspaces;
export type ISerializer = SerializerInterface;
}
export {serialization};

View File

@@ -56,67 +56,68 @@ export class Bubble implements IBubble {
static ANCHOR_RADIUS = 8;
/** Mouse up event data. */
private static onMouseUpWrapper_: browserEvents.Data|null = null;
private static onMouseUpWrapper: browserEvents.Data|null = null;
/** Mouse move event data. */
private static onMouseMoveWrapper_: browserEvents.Data|null = null;
private static onMouseMoveWrapper: browserEvents.Data|null = null;
workspace_: WorkspaceSvg;
content_: SVGElement;
shape_: SVGElement;
/** Flag to stop incremental rendering during construction. */
private readonly rendered_: boolean;
private readonly rendered: boolean;
/** The SVG group containing all parts of the bubble. */
private bubbleGroup_: SVGGElement|null = null;
private bubbleGroup: SVGGElement|null = null;
/**
* The SVG path for the arrow from the bubble to the icon on the block.
*/
private bubbleArrow_: SVGPathElement|null = null;
private bubbleArrow: SVGPathElement|null = null;
/** The SVG rect for the main body of the bubble. */
private bubbleBack_: SVGRectElement|null = null;
private bubbleBack: SVGRectElement|null = null;
/** The SVG group for the resize hash marks on some bubbles. */
private resizeGroup_: SVGGElement|null = null;
private resizeGroup: SVGGElement|null = null;
/** Absolute coordinate of anchor point, in workspace coordinates. */
private anchorXY_!: Coordinate;
private anchorXY!: Coordinate;
/**
* Relative X coordinate of bubble with respect to the anchor's centre,
* in workspace units.
* In RTL mode the initial value is negated.
*/
private relativeLeft_ = 0;
private relativeLeft = 0;
/**
* Relative Y coordinate of bubble with respect to the anchor's centre, in
* workspace units.
*/
private relativeTop_ = 0;
private relativeTop = 0;
/** Width of bubble, in workspace units. */
private width_ = 0;
private width = 0;
/** Height of bubble, in workspace units. */
private height_ = 0;
private height = 0;
/** Automatically position and reposition the bubble. */
private autoLayout_ = true;
private autoLayout = true;
/** Method to call on resize of bubble. */
private resizeCallback_: (() => void)|null = null;
private resizeCallback: (() => void)|null = null;
/** Method to call on move of bubble. */
private moveCallback_: (() => void)|null = null;
private moveCallback: (() => void)|null = null;
/** Mouse down on bubbleBack_ event data. */
private onMouseDownBubbleWrapper_: browserEvents.Data|null = null;
/** Mouse down on bubbleBack event data. */
private onMouseDownBubbleWrapper: browserEvents.Data|null = null;
/** Mouse down on resizeGroup_ event data. */
private onMouseDownResizeWrapper_: browserEvents.Data|null = null;
/** Mouse down on resizeGroup event data. */
private onMouseDownResizeWrapper: browserEvents.Data|null = null;
/**
* Describes whether this bubble has been disposed of (nodes and event
@@ -125,7 +126,7 @@ export class Bubble implements IBubble {
* @internal
*/
disposed = false;
private arrow_radians_: number;
private arrowRadians: number;
/**
* @param workspace The workspace on which to draw the bubble.
@@ -139,7 +140,7 @@ export class Bubble implements IBubble {
workspace: WorkspaceSvg, content: SVGElement, shape: SVGElement,
anchorXY: Coordinate, bubbleWidth: number|null,
bubbleHeight: number|null) {
this.rendered_ = false;
this.rendered = false;
this.workspace_ = workspace;
this.content_ = content;
this.shape_ = shape;
@@ -148,11 +149,11 @@ export class Bubble implements IBubble {
if (this.workspace_.RTL) {
angle = -angle;
}
this.arrow_radians_ = math.toRadians(angle);
this.arrowRadians = math.toRadians(angle);
const canvas = workspace.getBubbleCanvas();
canvas.appendChild(
this.createDom_(content, !!(bubbleWidth && bubbleHeight)));
this.createDom(content, !!(bubbleWidth && bubbleHeight)));
this.setAnchorLocation(anchorXY);
if (!bubbleWidth || !bubbleHeight) {
@@ -163,9 +164,9 @@ export class Bubble implements IBubble {
this.setBubbleSize(bubbleWidth, bubbleHeight);
// Render the bubble.
this.positionBubble_();
this.renderArrow_();
this.rendered_ = true;
this.positionBubble();
this.renderArrow();
this.rendered = true;
}
/**
@@ -175,7 +176,7 @@ export class Bubble implements IBubble {
* @param hasResize Add diagonal resize gripper if true.
* @returns The bubble's SVG group.
*/
private createDom_(content: Element, hasResize: boolean): SVGElement {
private createDom(content: Element, hasResize: boolean): SVGElement {
/* Create the bubble. Here's the markup that will be generated:
<g>
<g filter="url(#blocklyEmbossFilter837493)">
@@ -191,7 +192,7 @@ export class Bubble implements IBubble {
[...content goes here...]
</g>
*/
this.bubbleGroup_ = dom.createSvgElement(Svg.G, {});
this.bubbleGroup = dom.createSvgElement(Svg.G, {});
let filter: {filter?: string} = {
'filter': 'url(#' +
this.workspace_.getRenderer().getConstants().embossFilterId + ')',
@@ -201,9 +202,9 @@ export class Bubble implements IBubble {
// https://github.com/google/blockly/issues/99
filter = {};
}
const bubbleEmboss = dom.createSvgElement(Svg.G, filter, this.bubbleGroup_);
this.bubbleArrow_ = dom.createSvgElement(Svg.PATH, {}, bubbleEmboss);
this.bubbleBack_ = dom.createSvgElement(
const bubbleEmboss = dom.createSvgElement(Svg.G, filter, this.bubbleGroup);
this.bubbleArrow = dom.createSvgElement(Svg.PATH, {}, bubbleEmboss);
this.bubbleBack = dom.createSvgElement(
Svg.RECT, {
'class': 'blocklyDraggable',
'x': 0,
@@ -213,17 +214,17 @@ export class Bubble implements IBubble {
},
bubbleEmboss);
if (hasResize) {
this.resizeGroup_ = dom.createSvgElement(
this.resizeGroup = dom.createSvgElement(
Svg.G, {
'class': this.workspace_.RTL ? 'blocklyResizeSW' :
'blocklyResizeSE',
},
this.bubbleGroup_);
this.bubbleGroup);
const resizeSize = 2 * Bubble.BORDER_WIDTH;
dom.createSvgElement(
Svg.POLYGON,
{'points': '0,x x,x x,0'.replace(/x/g, resizeSize.toString())},
this.resizeGroup_);
this.resizeGroup);
dom.createSvgElement(
Svg.LINE, {
'class': 'blocklyResizeLine',
@@ -232,7 +233,7 @@ export class Bubble implements IBubble {
'x2': resizeSize - 1,
'y2': resizeSize / 3,
},
this.resizeGroup_);
this.resizeGroup);
dom.createSvgElement(
Svg.LINE, {
'class': 'blocklyResizeLine',
@@ -241,21 +242,21 @@ export class Bubble implements IBubble {
'x2': resizeSize - 1,
'y2': resizeSize * 2 / 3,
},
this.resizeGroup_);
this.resizeGroup);
} else {
this.resizeGroup_ = null;
this.resizeGroup = null;
}
if (!this.workspace_.options.readOnly) {
this.onMouseDownBubbleWrapper_ = browserEvents.conditionalBind(
this.bubbleBack_, 'mousedown', this, this.bubbleMouseDown_);
if (this.resizeGroup_) {
this.onMouseDownResizeWrapper_ = browserEvents.conditionalBind(
this.resizeGroup_, 'mousedown', this, this.resizeMouseDown_);
this.onMouseDownBubbleWrapper = browserEvents.conditionalBind(
this.bubbleBack, 'mousedown', this, this.bubbleMouseDown);
if (this.resizeGroup) {
this.onMouseDownResizeWrapper = browserEvents.conditionalBind(
this.resizeGroup, 'mousedown', this, this.resizeMouseDown);
}
}
this.bubbleGroup_.appendChild(content);
return this.bubbleGroup_;
this.bubbleGroup.appendChild(content);
return this.bubbleGroup;
}
/**
@@ -264,7 +265,7 @@ export class Bubble implements IBubble {
* @returns The root SVG node of the bubble's group.
*/
getSvgRoot(): SVGElement {
return this.bubbleGroup_ as SVGElement;
return this.bubbleGroup as SVGElement;
}
/**
@@ -273,7 +274,7 @@ export class Bubble implements IBubble {
* @param id ID of block.
*/
setSvgId(id: string) {
this.bubbleGroup_?.setAttribute('data-block-id', id);
this.bubbleGroup?.setAttribute('data-block-id', id);
}
/**
@@ -281,7 +282,7 @@ export class Bubble implements IBubble {
*
* @param e Mouse down event.
*/
private bubbleMouseDown_(e: Event) {
private bubbleMouseDown(e: Event) {
const gesture = this.workspace_.getGesture(e);
if (gesture) {
gesture.handleBubbleStart(e, this);
@@ -321,9 +322,9 @@ export class Bubble implements IBubble {
*
* @param e Mouse down event.
*/
private resizeMouseDown_(e: MouseEvent) {
private resizeMouseDown(e: MouseEvent) {
this.promote();
Bubble.unbindDragEvents_();
Bubble.unbindDragEvents();
if (browserEvents.isRightButton(e)) {
// No right-click.
e.stopPropagation();
@@ -333,12 +334,12 @@ export class Bubble implements IBubble {
this.workspace_.startDrag(
e,
new Coordinate(
this.workspace_.RTL ? -this.width_ : this.width_, this.height_));
this.workspace_.RTL ? -this.width : this.width, this.height));
Bubble.onMouseUpWrapper_ = browserEvents.conditionalBind(
document, 'mouseup', this, Bubble.bubbleMouseUp_);
Bubble.onMouseMoveWrapper_ = browserEvents.conditionalBind(
document, 'mousemove', this, this.resizeMouseMove_);
Bubble.onMouseUpWrapper = browserEvents.conditionalBind(
document, 'mouseup', this, Bubble.bubbleMouseUp);
Bubble.onMouseMoveWrapper = browserEvents.conditionalBind(
document, 'mousemove', this, this.resizeMouseMove);
this.workspace_.hideChaff();
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
@@ -349,13 +350,13 @@ export class Bubble implements IBubble {
*
* @param e Mouse move event.
*/
private resizeMouseMove_(e: MouseEvent) {
this.autoLayout_ = false;
private resizeMouseMove(e: MouseEvent) {
this.autoLayout = false;
const newXY = this.workspace_.moveDrag(e);
this.setBubbleSize(this.workspace_.RTL ? -newXY.x : newXY.x, newXY.y);
if (this.workspace_.RTL) {
// RTL requires the bubble to move its left edge.
this.positionBubble_();
this.positionBubble();
}
}
@@ -365,7 +366,7 @@ export class Bubble implements IBubble {
* @param callback The function to call on resize.
*/
registerResizeEvent(callback: () => void) {
this.resizeCallback_ = callback;
this.resizeCallback = callback;
}
/**
@@ -374,7 +375,7 @@ export class Bubble implements IBubble {
* @param callback The function to call on move.
*/
registerMoveEvent(callback: () => void) {
this.moveCallback_ = callback;
this.moveCallback = callback;
}
/**
@@ -384,9 +385,9 @@ export class Bubble implements IBubble {
* @internal
*/
promote(): boolean {
const svgGroup = this.bubbleGroup_?.parentNode;
if (svgGroup?.lastChild !== this.bubbleGroup_ && this.bubbleGroup_) {
svgGroup?.appendChild(this.bubbleGroup_);
const svgGroup = this.bubbleGroup?.parentNode;
if (svgGroup?.lastChild !== this.bubbleGroup && this.bubbleGroup) {
svgGroup?.appendChild(this.bubbleGroup);
return true;
}
return false;
@@ -399,29 +400,29 @@ export class Bubble implements IBubble {
* @param xy Absolute location.
*/
setAnchorLocation(xy: Coordinate) {
this.anchorXY_ = xy;
if (this.rendered_) {
this.positionBubble_();
this.anchorXY = xy;
if (this.rendered) {
this.positionBubble();
}
}
/** Position the bubble so that it does not fall off-screen. */
private layoutBubble_() {
private layoutBubble() {
// Get the metrics in workspace units.
const viewMetrics =
this.workspace_.getMetricsManager().getViewMetrics(true);
const optimalLeft = this.getOptimalRelativeLeft_(viewMetrics);
const optimalTop = this.getOptimalRelativeTop_(viewMetrics);
const optimalLeft = this.getOptimalRelativeLeft(viewMetrics);
const optimalTop = this.getOptimalRelativeTop(viewMetrics);
const bbox = (this.shape_ as SVGGraphicsElement).getBBox();
const topPosition = {
x: optimalLeft,
y: -this.height_ -
y: -this.height -
this.workspace_.getRenderer().getConstants().MIN_BLOCK_HEIGHT as
number,
};
const startPosition = {x: -this.width_ - 30, y: optimalTop};
const startPosition = {x: -this.width - 30, y: optimalTop};
const endPosition = {x: bbox.width, y: optimalTop};
const bottomPosition = {x: optimalLeft, y: bbox.height};
@@ -430,11 +431,11 @@ export class Bubble implements IBubble {
const fartherPosition =
bbox.width < bbox.height ? bottomPosition : endPosition;
const topPositionOverlap = this.getOverlap_(topPosition, viewMetrics);
const startPositionOverlap = this.getOverlap_(startPosition, viewMetrics);
const closerPositionOverlap = this.getOverlap_(closerPosition, viewMetrics);
const topPositionOverlap = this.getOverlap(topPosition, viewMetrics);
const startPositionOverlap = this.getOverlap(startPosition, viewMetrics);
const closerPositionOverlap = this.getOverlap(closerPosition, viewMetrics);
const fartherPositionOverlap =
this.getOverlap_(fartherPosition, viewMetrics);
this.getOverlap(fartherPosition, viewMetrics);
// Set the position to whichever position shows the most of the bubble,
// with tiebreaks going in the order: top > start > close > far.
@@ -442,25 +443,25 @@ export class Bubble implements IBubble {
topPositionOverlap, startPositionOverlap, closerPositionOverlap,
fartherPositionOverlap);
if (topPositionOverlap === mostOverlap) {
this.relativeLeft_ = topPosition.x;
this.relativeTop_ = topPosition.y;
this.relativeLeft = topPosition.x;
this.relativeTop = topPosition.y;
return;
}
if (startPositionOverlap === mostOverlap) {
this.relativeLeft_ = startPosition.x;
this.relativeTop_ = startPosition.y;
this.relativeLeft = startPosition.x;
this.relativeTop = startPosition.y;
return;
}
if (closerPositionOverlap === mostOverlap) {
this.relativeLeft_ = closerPosition.x;
this.relativeTop_ = closerPosition.y;
this.relativeLeft = closerPosition.x;
this.relativeTop = closerPosition.y;
return;
}
// TODO: I believe relativeLeft_ should actually be called relativeStart_
// and then the math should be fixed to reflect this. (hopefully it'll
// make it look simpler)
this.relativeLeft_ = fartherPosition.x;
this.relativeTop_ = fartherPosition.y;
this.relativeLeft = fartherPosition.x;
this.relativeTop = fartherPosition.y;
}
/**
@@ -473,19 +474,19 @@ export class Bubble implements IBubble {
* in.
* @returns The percentage of the bubble that is visible.
*/
private getOverlap_(
private getOverlap(
relativeMin: {x: number, y: number},
viewMetrics: ContainerRegion): number {
// The position of the top-left corner of the bubble in workspace units.
const bubbleMin = {
x: this.workspace_.RTL ? this.anchorXY_.x - relativeMin.x - this.width_ :
relativeMin.x + this.anchorXY_.x,
y: relativeMin.y + this.anchorXY_.y,
x: this.workspace_.RTL ? this.anchorXY.x - relativeMin.x - this.width :
relativeMin.x + this.anchorXY.x,
y: relativeMin.y + this.anchorXY.y,
};
// The position of the bottom-right corner of the bubble in workspace units.
const bubbleMax = {
x: bubbleMin.x + this.width_,
y: bubbleMin.y + this.height_,
x: bubbleMin.x + this.width,
y: bubbleMin.y + this.height,
};
// We could adjust these values to account for the scrollbars, but the
@@ -507,8 +508,7 @@ export class Bubble implements IBubble {
Math.max(bubbleMin.y, workspaceMin.y);
return Math.max(
0,
Math.min(
1, overlapWidth * overlapHeight / (this.width_ * this.height_)));
Math.min(1, overlapWidth * overlapHeight / (this.width * this.height)));
}
/**
@@ -521,18 +521,18 @@ export class Bubble implements IBubble {
* @returns The optimal horizontal position of the top-left corner of the
* bubble.
*/
private getOptimalRelativeLeft_(viewMetrics: ContainerRegion): number {
let relativeLeft = -this.width_ / 4;
private getOptimalRelativeLeft(viewMetrics: ContainerRegion): number {
let relativeLeft = -this.width / 4;
// No amount of sliding left or right will give us a better overlap.
if (this.width_ > viewMetrics.width) {
if (this.width > viewMetrics.width) {
return relativeLeft;
}
if (this.workspace_.RTL) {
// Bubble coordinates are flipped in RTL.
const bubbleRight = this.anchorXY_.x - relativeLeft;
const bubbleLeft = bubbleRight - this.width_;
const bubbleRight = this.anchorXY.x - relativeLeft;
const bubbleLeft = bubbleRight - this.width;
const workspaceRight = viewMetrics.left + viewMetrics.width;
const workspaceLeft = viewMetrics.left +
@@ -541,14 +541,14 @@ export class Bubble implements IBubble {
if (bubbleLeft < workspaceLeft) {
// Slide the bubble right until it is onscreen.
relativeLeft = -(workspaceLeft - this.anchorXY_.x + this.width_);
relativeLeft = -(workspaceLeft - this.anchorXY.x + this.width);
} else if (bubbleRight > workspaceRight) {
// Slide the bubble left until it is onscreen.
relativeLeft = -(workspaceRight - this.anchorXY_.x);
relativeLeft = -(workspaceRight - this.anchorXY.x);
}
} else {
const bubbleLeft = relativeLeft + this.anchorXY_.x;
const bubbleRight = bubbleLeft + this.width_;
const bubbleLeft = relativeLeft + this.anchorXY.x;
const bubbleRight = bubbleLeft + this.width;
const workspaceLeft = viewMetrics.left;
const workspaceRight = viewMetrics.left + viewMetrics.width -
@@ -557,10 +557,10 @@ export class Bubble implements IBubble {
if (bubbleLeft < workspaceLeft) {
// Slide the bubble right until it is onscreen.
relativeLeft = workspaceLeft - this.anchorXY_.x;
relativeLeft = workspaceLeft - this.anchorXY.x;
} else if (bubbleRight > workspaceRight) {
// Slide the bubble left until it is onscreen.
relativeLeft = workspaceRight - this.anchorXY_.x - this.width_;
relativeLeft = workspaceRight - this.anchorXY.x - this.width;
}
}
@@ -577,42 +577,42 @@ export class Bubble implements IBubble {
* @returns The optimal vertical position of the top-left corner of the
* bubble.
*/
private getOptimalRelativeTop_(viewMetrics: ContainerRegion): number {
let relativeTop = -this.height_ / 4;
private getOptimalRelativeTop(viewMetrics: ContainerRegion): number {
let relativeTop = -this.height / 4;
// No amount of sliding up or down will give us a better overlap.
if (this.height_ > viewMetrics.height) {
if (this.height > viewMetrics.height) {
return relativeTop;
}
const bubbleTop = this.anchorXY_.y + relativeTop;
const bubbleBottom = bubbleTop + this.height_;
const bubbleTop = this.anchorXY.y + relativeTop;
const bubbleBottom = bubbleTop + this.height;
const workspaceTop = viewMetrics.top;
const workspaceBottom = viewMetrics.top +
viewMetrics.height - // Thickness in workspace units.
Scrollbar.scrollbarThickness / this.workspace_.scale;
const anchorY = this.anchorXY_.y;
const anchorY = this.anchorXY.y;
if (bubbleTop < workspaceTop) {
// Slide the bubble down until it is onscreen.
relativeTop = workspaceTop - anchorY;
} else if (bubbleBottom > workspaceBottom) {
// Slide the bubble up until it is onscreen.
relativeTop = workspaceBottom - anchorY - this.height_;
relativeTop = workspaceBottom - anchorY - this.height;
}
return relativeTop;
}
/** Move the bubble to a location relative to the anchor's centre. */
private positionBubble_() {
let left = this.anchorXY_.x;
private positionBubble() {
let left = this.anchorXY.x;
if (this.workspace_.RTL) {
left -= this.relativeLeft_ + this.width_;
left -= this.relativeLeft + this.width;
} else {
left += this.relativeLeft_;
left += this.relativeLeft;
}
const top = this.relativeTop_ + this.anchorXY_.y;
const top = this.relativeTop + this.anchorXY.y;
this.moveTo(left, top);
}
@@ -624,7 +624,7 @@ export class Bubble implements IBubble {
* @internal
*/
moveTo(x: number, y: number) {
this.bubbleGroup_?.setAttribute(
this.bubbleGroup?.setAttribute(
'transform', 'translate(' + x + ',' + y + ')');
}
@@ -635,8 +635,8 @@ export class Bubble implements IBubble {
* @internal
*/
setDragging(adding: boolean) {
if (!adding && this.moveCallback_) {
this.moveCallback_();
if (!adding && this.moveCallback) {
this.moveCallback();
}
}
@@ -646,7 +646,7 @@ export class Bubble implements IBubble {
* @returns The height and width of the bubble.
*/
getBubbleSize(): Size {
return new Size(this.width_, this.height_);
return new Size(this.width, this.height);
}
/**
@@ -660,46 +660,46 @@ export class Bubble implements IBubble {
// Minimum size of a bubble.
width = Math.max(width, doubleBorderWidth + 45);
height = Math.max(height, doubleBorderWidth + 20);
this.width_ = width;
this.height_ = height;
this.bubbleBack_?.setAttribute('width', width.toString());
this.bubbleBack_?.setAttribute('height', height.toString());
if (this.resizeGroup_) {
this.width = width;
this.height = height;
this.bubbleBack?.setAttribute('width', width.toString());
this.bubbleBack?.setAttribute('height', height.toString());
if (this.resizeGroup) {
if (this.workspace_.RTL) {
// Mirror the resize group.
const resizeSize = 2 * Bubble.BORDER_WIDTH;
this.resizeGroup_.setAttribute(
this.resizeGroup.setAttribute(
'transform',
'translate(' + resizeSize + ',' + (height - doubleBorderWidth) +
') scale(-1 1)');
} else {
this.resizeGroup_.setAttribute(
this.resizeGroup.setAttribute(
'transform',
'translate(' + (width - doubleBorderWidth) + ',' +
(height - doubleBorderWidth) + ')');
}
}
if (this.autoLayout_) {
this.layoutBubble_();
if (this.autoLayout) {
this.layoutBubble();
}
this.positionBubble_();
this.renderArrow_();
this.positionBubble();
this.renderArrow();
// Allow the contents to resize.
if (this.resizeCallback_) {
this.resizeCallback_();
if (this.resizeCallback) {
this.resizeCallback();
}
}
/** Draw the arrow between the bubble and the origin. */
private renderArrow_() {
private renderArrow() {
const steps = [];
// Find the relative coordinates of the center of the bubble.
const relBubbleX = this.width_ / 2;
const relBubbleY = this.height_ / 2;
const relBubbleX = this.width / 2;
const relBubbleY = this.height / 2;
// Find the relative coordinates of the center of the anchor.
let relAnchorX = -this.relativeLeft_;
let relAnchorY = -this.relativeTop_;
let relAnchorX = -this.relativeLeft;
let relAnchorY = -this.relativeTop;
if (relBubbleX === relAnchorX && relBubbleY === relAnchorY) {
// Null case. Bubble is directly on top of the anchor.
// Short circuit this rather than wade through divide by zeros.
@@ -742,7 +742,7 @@ export class Bubble implements IBubble {
const baseY2 = relBubbleY - thickness * rightRise;
// Distortion to curve the arrow.
let swirlAngle = angle + this.arrow_radians_;
let swirlAngle = angle + this.arrowRadians;
if (swirlAngle > Math.PI * 2) {
swirlAngle -= Math.PI * 2;
}
@@ -758,7 +758,7 @@ export class Bubble implements IBubble {
',' + (baseY2 + swirlRise) + ' ' + baseX2 + ',' + baseY2);
}
steps.push('z');
this.bubbleArrow_?.setAttribute('d', steps.join(' '));
this.bubbleArrow?.setAttribute('d', steps.join(' '));
}
/**
@@ -767,20 +767,20 @@ export class Bubble implements IBubble {
* @param hexColour Hex code of colour.
*/
setColour(hexColour: string) {
this.bubbleBack_?.setAttribute('fill', hexColour);
this.bubbleArrow_?.setAttribute('fill', hexColour);
this.bubbleBack?.setAttribute('fill', hexColour);
this.bubbleArrow?.setAttribute('fill', hexColour);
}
/** Dispose of this bubble. */
dispose() {
if (this.onMouseDownBubbleWrapper_) {
browserEvents.unbind(this.onMouseDownBubbleWrapper_);
if (this.onMouseDownBubbleWrapper) {
browserEvents.unbind(this.onMouseDownBubbleWrapper);
}
if (this.onMouseDownResizeWrapper_) {
browserEvents.unbind(this.onMouseDownResizeWrapper_);
if (this.onMouseDownResizeWrapper) {
browserEvents.unbind(this.onMouseDownResizeWrapper);
}
Bubble.unbindDragEvents_();
dom.removeNode(this.bubbleGroup_);
Bubble.unbindDragEvents();
dom.removeNode(this.bubbleGroup);
this.disposed = true;
}
@@ -800,12 +800,12 @@ export class Bubble implements IBubble {
this.moveTo(newLoc.x, newLoc.y);
}
if (this.workspace_.RTL) {
this.relativeLeft_ = this.anchorXY_.x - newLoc.x - this.width_;
this.relativeLeft = this.anchorXY.x - newLoc.x - this.width;
} else {
this.relativeLeft_ = newLoc.x - this.anchorXY_.x;
this.relativeLeft = newLoc.x - this.anchorXY.x;
}
this.relativeTop_ = newLoc.y - this.anchorXY_.y;
this.renderArrow_();
this.relativeTop = newLoc.y - this.anchorXY.y;
this.renderArrow();
}
/**
@@ -817,9 +817,9 @@ export class Bubble implements IBubble {
getRelativeToSurfaceXY(): Coordinate {
return new Coordinate(
this.workspace_.RTL ?
-this.relativeLeft_ + this.anchorXY_.x - this.width_ :
this.anchorXY_.x + this.relativeLeft_,
this.anchorXY_.y + this.relativeTop_);
-this.relativeLeft + this.anchorXY.x - this.width :
this.anchorXY.x + this.relativeLeft,
this.anchorXY.y + this.relativeTop);
}
/**
@@ -831,18 +831,18 @@ export class Bubble implements IBubble {
* @internal
*/
setAutoLayout(enable: boolean) {
this.autoLayout_ = enable;
this.autoLayout = enable;
}
/** Stop binding to the global mouseup and mousemove events. */
private static unbindDragEvents_() {
if (Bubble.onMouseUpWrapper_) {
browserEvents.unbind(Bubble.onMouseUpWrapper_);
Bubble.onMouseUpWrapper_ = null;
private static unbindDragEvents() {
if (Bubble.onMouseUpWrapper) {
browserEvents.unbind(Bubble.onMouseUpWrapper);
Bubble.onMouseUpWrapper = null;
}
if (Bubble.onMouseMoveWrapper_) {
browserEvents.unbind(Bubble.onMouseMoveWrapper_);
Bubble.onMouseMoveWrapper_ = null;
if (Bubble.onMouseMoveWrapper) {
browserEvents.unbind(Bubble.onMouseMoveWrapper);
Bubble.onMouseMoveWrapper = null;
}
}
@@ -851,9 +851,9 @@ export class Bubble implements IBubble {
*
* @param _e Mouse up event.
*/
private static bubbleMouseUp_(_e: MouseEvent) {
private static bubbleMouseUp(_e: MouseEvent) {
Touch.clearTouchIdentifier();
Bubble.unbindDragEvents_();
Bubble.unbindDragEvents();
}
/**

View File

@@ -147,16 +147,16 @@ function extractObjectFromEvent(
switch (e.type) {
case eventUtils.BLOCK_CREATE:
case eventUtils.BLOCK_MOVE:
object = workspace.getBlockById((e as BlockCreate | BlockMove).blockId);
object = workspace.getBlockById((e as BlockCreate | BlockMove).blockId!);
if (object) {
object = object.getRootBlock();
}
break;
case eventUtils.COMMENT_CREATE:
case eventUtils.COMMENT_MOVE:
object = workspace.getCommentById(
(e as CommentCreate | CommentMove).commentId) as
WorkspaceCommentSvg |
object =
workspace.getCommentById((e as CommentCreate | CommentMove).commentId!
) as WorkspaceCommentSvg |
null;
break;
}

View File

@@ -142,7 +142,7 @@ export class Comment extends Icon {
HTMLTextAreaElement;
const textarea = this.textarea_;
textarea.className = 'blocklyCommentTextarea';
textarea.setAttribute('dir', this.block_.RTL ? 'RTL' : 'LTR');
textarea.setAttribute('dir', this.getBlock().RTL ? 'RTL' : 'LTR');
textarea.value = this.model_.text ?? '';
this.resizeTextarea_();
@@ -167,7 +167,7 @@ export class Comment extends Icon {
function(this: Comment, _e: Event) {
if (this.cachedText_ !== this.model_.text) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this.block_, 'comment', null, this.cachedText_,
this.getBlock(), 'comment', null, this.cachedText_,
this.model_.text));
}
});
@@ -233,7 +233,7 @@ export class Comment extends Icon {
return;
}
eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
this.block_, visible, 'comment'));
this.getBlock(), visible, 'comment'));
this.model_.pinned = visible;
if (visible) {
this.createBubble_();
@@ -244,7 +244,7 @@ export class Comment extends Icon {
/** Show the bubble. Handles deciding if it should be editable or not. */
private createBubble_() {
if (!this.block_.isEditable()) {
if (!this.getBlock().isEditable()) {
this.createNonEditableBubble_();
} else {
this.createEditableBubble_();
@@ -253,12 +253,13 @@ export class Comment extends Icon {
/** Show an editable bubble. */
private createEditableBubble_() {
const block = this.getBlock();
this.bubble_ = new Bubble(
this.block_.workspace, this.createEditor_(),
this.block_.pathObject.svgPath, (this.iconXY_ as Coordinate),
this.model_.size.width, this.model_.size.height);
block.workspace, this.createEditor_(), block.pathObject.svgPath,
(this.iconXY_ as Coordinate), this.model_.size.width,
this.model_.size.height);
// Expose this comment's block's ID on its top-level SVG group.
this.bubble_.setSvgId(this.block_.id);
this.bubble_.setSvgId(block.id);
this.bubble_.registerResizeEvent(this.onBubbleResize_.bind(this));
this.applyColour();
}
@@ -272,7 +273,7 @@ export class Comment extends Icon {
// TODO (#2917): It would be great if the comment could support line breaks.
this.paragraphElement_ = Bubble.textToDom(this.model_.text ?? '');
this.bubble_ = Bubble.createNonEditableBubble(
this.paragraphElement_, (this.block_), this.iconXY_ as Coordinate);
this.paragraphElement_, this.getBlock(), this.iconXY_ as Coordinate);
this.applyColour();
}
@@ -371,7 +372,7 @@ export class Comment extends Icon {
* should not be called directly. Instead call block.setCommentText(null);
*/
override dispose() {
this.block_.comment = null;
this.getBlock().comment = null;
super.dispose();
}
}

View File

@@ -571,7 +571,7 @@ export class Connection implements IASTNodeLocationWithBlock {
const parentBlock = this.getSourceBlock();
const shadowState = this.getShadowState();
const shadowDom = this.getShadowDom();
if (parentBlock.disposed || !shadowState && !shadowDom) {
if (parentBlock.isDeadOrDying() || !shadowState && !shadowDom) {
return null;
}

View File

@@ -17,6 +17,7 @@ import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as clipboard from './clipboard.js';
import {config} from './config.js';
import * as dom from './utils/dom.js';
import type {ContextMenuOption, LegacyContextMenuOption} from './contextmenu_registry.js';
import * as eventUtils from './events/utils.js';
import {Menu} from './menu.js';
@@ -179,7 +180,7 @@ function createWidget_(menu: Menu) {
throw Error('Attempting to create a context menu when widget div is null');
}
const menuDom = menu.render(div);
menuDom.classList.add('blocklyContextMenu');
dom.addClass(menuDom, 'blocklyContextMenu');
// Prevent system context menu when right-clicking a Blockly context menu.
browserEvents.conditionalBind(
(menuDom as EventTarget), 'contextmenu', null, haltPropagation);

View File

@@ -249,7 +249,7 @@ function deleteNext_(deleteList: BlockSvg[], eventGroup: string) {
eventUtils.setGroup(eventGroup);
const block = deleteList.shift();
if (block) {
if (!block.disposed) {
if (!block.isDeadOrDying()) {
block.dispose(false, true);
setTimeout(deleteNext_, DELAY, deleteList, eventGroup);
} else {

View File

@@ -12,8 +12,6 @@
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Css');
import * as deprecation from './utils/deprecation.js';
/** Has CSS already been injected? */
let injected = false;
@@ -25,20 +23,11 @@ let injected = false;
* @param cssContent Multiline CSS string or an array of single lines of CSS.
* @alias Blockly.Css.register
*/
export function register(cssContent: string|string[]) {
export function register(cssContent: string) {
if (injected) {
throw Error('CSS already injected');
}
if (Array.isArray(cssContent)) {
deprecation.warn(
'Registering CSS by passing an array of strings', 'September 2021',
'September 2022', 'css.register passing a multiline string');
content += '\n' + cssContent.join('\n');
} else {
// Add new cssContent in the global content.
content += '\n' + cssContent;
}
content += '\n' + cssContent;
}
/**
@@ -171,7 +160,7 @@ let content = `
}
.blocklyDropDownContent {
max-height: 300px; // @todo: spec for maximum height.
max-height: 300px; /* @todo: spec for maximum height. */
overflow: auto;
overflow-x: hidden;
position: relative;

View File

@@ -15,6 +15,7 @@ goog.declareModuleId('Blockly.dropDownDiv');
import type {BlockSvg} from './block_svg.js';
import * as common from './common.js';
import * as dom from './utils/dom.js';
import type {Field} from './field.js';
import * as math from './utils/math.js';
import {Rect} from './utils/rect.js';
@@ -138,10 +139,10 @@ export function createDom() {
// Handle focusin/out events to add a visual indicator when
// a child is focused or blurred.
div.addEventListener('focusin', function() {
div.classList.add('blocklyFocused');
dom.addClass(div, 'blocklyFocused');
});
div.addEventListener('focusout', function() {
div.classList.remove('blocklyFocused');
dom.removeClass(div, 'blocklyFocused');
});
}
@@ -311,10 +312,10 @@ export function show(
renderedClassName = mainWorkspace.getRenderer().getClassName();
themeClassName = mainWorkspace.getTheme().getClassName();
if (renderedClassName) {
div.classList.add(renderedClassName);
dom.addClass(div, renderedClassName);
}
if (themeClassName) {
div.classList.add(themeClassName);
dom.addClass(div, themeClassName);
}
// When we change `translate` multiple times in close succession,
@@ -595,11 +596,11 @@ export function hideWithoutAnimation() {
owner = null;
if (renderedClassName) {
div.classList.remove(renderedClassName);
dom.removeClass(div, renderedClassName);
renderedClassName = '';
}
if (themeClassName) {
div.classList.remove(themeClassName);
dom.removeClass(div, themeClassName);
themeClassName = '';
}
(common.getMainWorkspace() as WorkspaceSvg).markFocused();

View File

@@ -13,64 +13,90 @@ import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events');
import {Abstract as AbstractEvent} from './events_abstract.js';
import {BlockBase} from './events_block_base.js';
import {BlockChange} from './events_block_change.js';
import {BlockCreate} from './events_block_create.js';
import {BlockDelete} from './events_block_delete.js';
import {BlockDrag} from './events_block_drag.js';
import {BlockMove} from './events_block_move.js';
import {BubbleOpen} from './events_bubble_open.js';
import {Click} from './events_click.js';
import {CommentBase} from './events_comment_base.js';
import {CommentChange} from './events_comment_change.js';
import {CommentCreate} from './events_comment_create.js';
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';
import {BlockChange, BlockChangeJson} from './events_block_change.js';
import {BlockCreate, BlockCreateJson} from './events_block_create.js';
import {BlockDelete, BlockDeleteJson} from './events_block_delete.js';
import {BlockDrag, BlockDragJson} from './events_block_drag.js';
import {BlockMove, BlockMoveJson} from './events_block_move.js';
import {BubbleOpen, BubbleOpenJson, BubbleType} from './events_bubble_open.js';
import {Click, ClickJson, ClickTarget} from './events_click.js';
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
import {CommentChange, CommentChangeJson} from './events_comment_change.js';
import {CommentCreate, CommentCreateJson} from './events_comment_create.js';
import {CommentDelete} from './events_comment_delete.js';
import {CommentMove} from './events_comment_move.js';
import {MarkerMove} from './events_marker_move.js';
import {Selected} from './events_selected.js';
import {ThemeChange} from './events_theme_change.js';
import {ToolboxItemSelect} from './events_toolbox_item_select.js';
import {TrashcanOpen} from './events_trashcan_open.js';
import {CommentMove, CommentMoveJson} from './events_comment_move.js';
import {MarkerMove, MarkerMoveJson} from './events_marker_move.js';
import {Selected, SelectedJson} from './events_selected.js';
import {ThemeChange, ThemeChangeJson} from './events_theme_change.js';
import {ToolboxItemSelect, ToolboxItemSelectJson} from './events_toolbox_item_select.js';
import {TrashcanOpen, TrashcanOpenJson} from './events_trashcan_open.js';
import {Ui} from './events_ui.js';
import {UiBase} from './events_ui_base.js';
import {VarBase} from './events_var_base.js';
import {VarCreate} from './events_var_create.js';
import {VarDelete} from './events_var_delete.js';
import {VarRename} from './events_var_rename.js';
import {ViewportChange} from './events_viewport.js';
import {VarBase, VarBaseJson} from './events_var_base.js';
import {VarCreate, VarCreateJson} from './events_var_create.js';
import {VarDelete, VarDeleteJson} from './events_var_delete.js';
import {VarRename, VarRenameJson} from './events_var_rename.js';
import {ViewportChange, ViewportChangeJson} from './events_viewport.js';
import * as eventUtils from './utils.js';
import {FinishedLoading} from './workspace_events.js';
import {FinishedLoading, FinishedLoadingJson} from './workspace_events.js';
// Events.
export const Abstract = AbstractEvent;
export {AbstractEventJson};
export {BubbleOpen};
export {BubbleOpenJson};
export {BubbleType};
export {BlockBase};
export {BlockBaseJson};
export {BlockChange};
export {BlockChangeJson};
export {BlockCreate};
export {BlockCreateJson};
export {BlockDelete};
export {BlockDeleteJson};
export {BlockDrag};
export {BlockDragJson};
export {BlockMove};
export {BlockMoveJson};
export {Click};
export {ClickJson};
export {ClickTarget};
export {CommentBase};
export {CommentBaseJson};
export {CommentChange};
export {CommentChangeJson};
export {CommentCreate};
export {CommentCreateJson};
export {CommentDelete};
export {CommentMove};
export {CommentMoveJson};
export {FinishedLoading};
export {FinishedLoadingJson};
export {MarkerMove};
export {MarkerMoveJson};
export {Selected};
export {SelectedJson};
export {ThemeChange};
export {ThemeChangeJson};
export {ToolboxItemSelect};
export {ToolboxItemSelectJson};
export {TrashcanOpen};
export {TrashcanOpenJson};
export {Ui};
export {UiBase};
export {VarBase};
export {VarBaseJson};
export {VarCreate};
export {VarCreateJson};
export {VarDelete};
export {VarDeleteJson};
export {VarRename};
export {VarRenameJson};
export {ViewportChange};
export {ViewportChangeJson};
// Event types.
export const BLOCK_CHANGE = eventUtils.BLOCK_CHANGE;

View File

@@ -26,7 +26,7 @@ import * as eventUtils from './utils.js';
*/
export abstract class Abstract {
/** Whether or not the event is blank (to be populated by fromJson). */
isBlank: boolean|null = null;
abstract isBlank: boolean;
/** The workspace identifier for this event. */
workspaceId?: string = undefined;
@@ -37,7 +37,7 @@ export abstract class Abstract {
isUiEvent = false;
/** Type of this event. */
type?: string = undefined;
type = '';
/** @alias Blockly.Events.Abstract */
constructor() {
@@ -57,12 +57,11 @@ export abstract class Abstract {
*
* @returns JSON representation.
*/
toJson(): AnyDuringMigration {
const json = {'type': this.type};
if (this.group) {
(json as AnyDuringMigration)['group'] = this.group;
}
return json;
toJson(): AbstractEventJson {
return {
'type': this.type,
'group': this.group,
};
}
/**
@@ -70,9 +69,9 @@ export abstract class Abstract {
*
* @param json JSON representation.
*/
fromJson(json: AnyDuringMigration) {
fromJson(json: AbstractEventJson) {
this.isBlank = false;
this.group = json['group'];
this.group = json['group'] || '';
}
/**
@@ -112,3 +111,8 @@ export abstract class Abstract {
return workspace;
}
}
export interface AbstractEventJson {
type: string;
group: string;
}

View File

@@ -14,7 +14,7 @@ goog.declareModuleId('Blockly.Events.BlockBase');
import type {Block} from '../block.js';
import {Abstract as AbstractEvent} from './events_abstract.js';
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js';
/**
@@ -23,9 +23,8 @@ import {Abstract as AbstractEvent} from './events_abstract.js';
* @alias Blockly.Events.BlockBase
*/
export class BlockBase extends AbstractEvent {
override isBlank: AnyDuringMigration;
blockId: string;
override workspaceId: string;
override isBlank = true;
blockId?: string;
/**
* @param opt_block The block this event corresponds to.
@@ -33,13 +32,15 @@ export class BlockBase extends AbstractEvent {
*/
constructor(opt_block?: Block) {
super();
this.isBlank = typeof opt_block === 'undefined';
this.isBlank = !!opt_block;
if (!opt_block) return;
/** The block ID for the block this event pertains to */
this.blockId = this.isBlank ? '' : opt_block!.id;
this.blockId = opt_block.id;
/** The workspace identifier for this event. */
this.workspaceId = this.isBlank ? '' : opt_block!.workspace.id;
this.workspaceId = opt_block.workspace.id;
}
/**
@@ -47,8 +48,13 @@ export class BlockBase extends AbstractEvent {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): AbstractEventJson {
const json = super.toJson() as BlockBaseJson;
if (!this.blockId) {
throw new Error(
'The block ID is undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
json['blockId'] = this.blockId;
return json;
}
@@ -58,8 +64,12 @@ export class BlockBase extends AbstractEvent {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: BlockBaseJson) {
super.fromJson(json);
this.blockId = json['blockId'];
}
}
export interface BlockBaseJson extends AbstractEventJson {
blockId: string;
}

View File

@@ -17,7 +17,7 @@ import type {BlockSvg} from '../block_svg.js';
import * as registry from '../registry.js';
import * as Xml from '../xml.js';
import {BlockBase} from './events_block_base.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';
import * as eventUtils from './utils.js';
@@ -27,13 +27,11 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.BlockChange
*/
export class BlockChange extends BlockBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
element!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
name!: string|null;
oldValue: AnyDuringMigration;
newValue: AnyDuringMigration;
override type = eventUtils.BLOCK_CHANGE;
element?: string;
name?: string;
oldValue: unknown;
newValue: unknown;
/**
* @param opt_block The changed block. Undefined for a blank event.
@@ -44,19 +42,16 @@ export class BlockChange extends BlockBase {
*/
constructor(
opt_block?: Block, opt_element?: string, opt_name?: string|null,
opt_oldValue?: AnyDuringMigration, opt_newValue?: AnyDuringMigration) {
opt_oldValue?: unknown, opt_newValue?: unknown) {
super(opt_block);
/** Type of this event. */
this.type = eventUtils.BLOCK_CHANGE;
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
this.element = typeof opt_element === 'undefined' ? '' : opt_element;
this.name = typeof opt_name === 'undefined' ? '' : opt_name;
this.oldValue = typeof opt_oldValue === 'undefined' ? '' : opt_oldValue;
this.newValue = typeof opt_newValue === 'undefined' ? '' : opt_newValue;
this.element = opt_element;
this.name = opt_name || undefined;
this.oldValue = opt_oldValue;
this.newValue = opt_newValue;
}
/**
@@ -64,12 +59,15 @@ export class BlockChange extends BlockBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['element'] = this.element;
if (this.name) {
json['name'] = this.name;
override toJson(): BlockChangeJson {
const json = super.toJson() as BlockChangeJson;
if (!this.element) {
throw new Error(
'The changed element is undefined. Either pass an ' +
'element to the constructor, or call fromJson');
}
json['element'] = this.element;
json['name'] = this.name;
json['oldValue'] = this.oldValue;
json['newValue'] = this.newValue;
return json;
@@ -80,7 +78,7 @@ export class BlockChange extends BlockBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: BlockChangeJson) {
super.fromJson(json);
this.element = json['element'];
this.name = json['name'];
@@ -104,10 +102,16 @@ export class BlockChange extends BlockBase {
*/
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (!this.blockId) {
throw new Error(
'The block ID is undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
const block = workspace.getBlockById(this.blockId);
if (!block) {
console.warn('Can\'t change non-existent block: ' + this.blockId);
return;
throw new Error(
'The associated block is undefined. Either pass a ' +
'block to the constructor, or call fromJson');
}
// Assume the block is rendered so that then we can check.
const blockSvg = block as BlockSvg;
@@ -176,4 +180,11 @@ export class BlockChange extends BlockBase {
}
}
export interface BlockChangeJson extends BlockBaseJson {
element: string;
name?: string;
newValue: unknown;
oldValue: unknown;
}
registry.register(registry.Type.EVENT, eventUtils.CHANGE, BlockChange);

View File

@@ -17,7 +17,7 @@ import * as registry from '../registry.js';
import * as blocks from '../serialization/blocks.js';
import * as Xml from '../xml.js';
import {BlockBase} from './events_block_base.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';
import * as eventUtils from './utils.js';
@@ -27,20 +27,15 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.BlockCreate
*/
export class BlockCreate extends BlockBase {
override type: string;
xml: AnyDuringMigration;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
ids!: string[];
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
json!: blocks.State;
override type = eventUtils.BLOCK_CREATE;
xml?: Element|DocumentFragment;
ids?: string[];
json?: blocks.State;
/** @param opt_block The created block. Undefined for a blank event. */
constructor(opt_block?: Block) {
super(opt_block);
/** Type of this event. */
this.type = eventUtils.BLOCK_CREATE;
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
@@ -62,8 +57,23 @@ export class BlockCreate extends BlockBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): BlockCreateJson {
const json = super.toJson() as BlockCreateJson;
if (!this.xml) {
throw new Error(
'The block XML is undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
if (!this.ids) {
throw new Error(
'The block IDs are undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
if (!this.json) {
throw new Error(
'The block JSON is undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
json['xml'] = Xml.domToText(this.xml);
json['ids'] = this.ids;
json['json'] = this.json;
@@ -78,7 +88,7 @@ export class BlockCreate extends BlockBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: BlockCreateJson) {
super.fromJson(json);
this.xml = Xml.textToDom(json['xml']);
this.ids = json['ids'];
@@ -95,6 +105,16 @@ export class BlockCreate extends BlockBase {
*/
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (!this.json) {
throw new Error(
'The block JSON is undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
if (!this.ids) {
throw new Error(
'The block IDs are undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
if (forward) {
blocks.append(this.json, workspace);
} else {
@@ -112,4 +132,11 @@ export class BlockCreate extends BlockBase {
}
}
export interface BlockCreateJson extends BlockBaseJson {
xml: string;
ids: string[];
json: object;
recordUndo?: boolean;
}
registry.register(registry.Type.EVENT, eventUtils.CREATE, BlockCreate);

View File

@@ -17,7 +17,7 @@ import * as registry from '../registry.js';
import * as blocks from '../serialization/blocks.js';
import * as Xml from '../xml.js';
import {BlockBase} from './events_block_base.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';
import * as eventUtils from './utils.js';
@@ -27,22 +27,16 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.BlockDelete
*/
export class BlockDelete extends BlockBase {
override type: string;
oldXml: AnyDuringMigration;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
ids!: string[];
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
wasShadow!: boolean;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldJson!: blocks.State;
oldXml?: Element|DocumentFragment;
ids?: string[];
wasShadow?: boolean;
oldJson?: blocks.State;
override type = eventUtils.BLOCK_DELETE;
/** @param opt_block The deleted block. Undefined for a blank event. */
constructor(opt_block?: Block) {
super(opt_block);
/** Type of this event. */
this.type = eventUtils.BLOCK_DELETE;
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
@@ -71,8 +65,28 @@ export class BlockDelete extends BlockBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): BlockDeleteJson {
const json = super.toJson() as BlockDeleteJson;
if (!this.oldXml) {
throw new Error(
'The old block XML is undefined. Either pass a block ' +
'to the constructor, or call fromJson');
}
if (!this.ids) {
throw new Error(
'The block IDs are undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
if (this.wasShadow === undefined) {
throw new Error(
'Whether the block was a shadow is undefined. Either ' +
'pass a block to the constructor, or call fromJson');
}
if (!this.oldJson) {
throw new Error(
'The old block JSON is undefined. Either pass a block ' +
'to the constructor, or call fromJson');
}
json['oldXml'] = Xml.domToText(this.oldXml);
json['ids'] = this.ids;
json['wasShadow'] = this.wasShadow;
@@ -88,13 +102,13 @@ export class BlockDelete extends BlockBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: BlockDeleteJson) {
super.fromJson(json);
this.oldXml = Xml.textToDom(json['oldXml']);
this.ids = json['ids'];
this.wasShadow =
json['wasShadow'] || this.oldXml.tagName.toLowerCase() === 'shadow';
this.oldJson = json['oldJson'] as blocks.State;
this.oldJson = json['oldJson'];
if (json['recordUndo'] !== undefined) {
this.recordUndo = json['recordUndo'];
}
@@ -107,6 +121,16 @@ export class BlockDelete extends BlockBase {
*/
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (!this.ids) {
throw new Error(
'The block IDs are undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
if (!this.oldJson) {
throw new Error(
'The old block JSON is undefined. Either pass a block ' +
'to the constructor, or call fromJson');
}
if (forward) {
for (let i = 0; i < this.ids.length; i++) {
const id = this.ids[i];
@@ -124,4 +148,12 @@ export class BlockDelete extends BlockBase {
}
}
export interface BlockDeleteJson extends BlockBaseJson {
oldXml: string;
ids: string[];
wasShadow: boolean;
oldJson: blocks.State;
recordUndo?: boolean;
}
registry.register(registry.Type.EVENT, eventUtils.DELETE, BlockDelete);

View File

@@ -14,7 +14,7 @@ goog.declareModuleId('Blockly.Events.BlockDrag');
import type {Block} from '../block.js';
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
@@ -25,10 +25,10 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.BlockDrag
*/
export class BlockDrag extends UiBase {
blockId: AnyDuringMigration;
blockId?: string;
isStart?: boolean;
blocks?: Block[];
override type: string;
override type = eventUtils.BLOCK_DRAG;
/**
* @param opt_block The top block in the stack that is being dragged.
@@ -41,16 +41,15 @@ export class BlockDrag extends UiBase {
constructor(opt_block?: Block, opt_isStart?: boolean, opt_blocks?: Block[]) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined;
super(workspaceId);
this.blockId = opt_block ? opt_block.id : null;
if (!opt_block) return;
this.blockId = opt_block.id;
/** Whether this is the start of a block drag. */
this.isStart = opt_isStart;
/** The blocks affected by this drag event. */
this.blocks = opt_blocks;
/** Type of this event. */
this.type = eventUtils.BLOCK_DRAG;
}
/**
@@ -58,10 +57,22 @@ export class BlockDrag extends UiBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): BlockDragJson {
const json = super.toJson() as BlockDragJson;
if (this.isStart === undefined) {
throw new Error(
'Whether this event is the start of a drag is undefined. ' +
'Either pass the value to the constructor, or call fromJson');
}
if (this.blockId === undefined) {
throw new Error(
'The block ID is undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
json['isStart'] = this.isStart;
json['blockId'] = this.blockId;
// TODO: I don't think we should actually apply the blocks array to the JSON
// object b/c they have functions and aren't actually serializable.
json['blocks'] = this.blocks;
return json;
}
@@ -71,7 +82,7 @@ export class BlockDrag extends UiBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: BlockDragJson) {
super.fromJson(json);
this.isStart = json['isStart'];
this.blockId = json['blockId'];
@@ -79,4 +90,10 @@ export class BlockDrag extends UiBase {
}
}
export interface BlockDragJson extends AbstractEventJson {
isStart: boolean;
blockId: string;
blocks?: Block[];
}
registry.register(registry.Type.EVENT, eventUtils.BLOCK_DRAG, BlockDrag);

View File

@@ -17,15 +17,15 @@ import {ConnectionType} from '../connection_type.js';
import * as registry from '../registry.js';
import {Coordinate} from '../utils/coordinate.js';
import {BlockBase} from './events_block_base.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';
import * as eventUtils from './utils.js';
interface BlockLocation {
parentId: string;
inputName: string;
coordinate: Coordinate|null;
} // eslint-disable-line no-unused-vars
parentId?: string;
inputName?: string;
coordinate?: Coordinate;
}
/**
* Class for a block move event. Created before the move.
@@ -33,25 +33,19 @@ interface BlockLocation {
* @alias Blockly.Events.BlockMove
*/
export class BlockMove extends BlockBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldParentId!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldInputName!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldCoordinate!: Coordinate|null;
override type = eventUtils.BLOCK_MOVE;
oldParentId?: string;
oldInputName?: string;
oldCoordinate?: Coordinate;
newParentId: string|null = null;
newInputName: string|null = null;
newCoordinate: Coordinate|null = null;
newParentId?: string;
newInputName?: string;
newCoordinate?: Coordinate;
/** @param opt_block The moved block. Undefined for a blank event. */
constructor(opt_block?: Block) {
super(opt_block);
/** Type of this event. */
this.type = eventUtils.BLOCK_MOVE;
if (!opt_block) {
return;
}
@@ -72,17 +66,13 @@ export class BlockMove extends BlockBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
if (this.newParentId) {
json['newParentId'] = this.newParentId;
}
if (this.newInputName) {
json['newInputName'] = this.newInputName;
}
override toJson(): BlockMoveJson {
const json = super.toJson() as BlockMoveJson;
json['newParentId'] = this.newParentId;
json['newInputName'] = this.newInputName;
if (this.newCoordinate) {
json['newCoordinate'] = Math.round(this.newCoordinate.x) + ',' +
Math.round(this.newCoordinate.y);
json['newCoordinate'] = `${Math.round(this.newCoordinate.x)}, ` +
`${Math.round(this.newCoordinate.y)}`;
}
if (!this.recordUndo) {
json['recordUndo'] = this.recordUndo;
@@ -95,7 +85,7 @@ export class BlockMove extends BlockBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: BlockMoveJson) {
super.fromJson(json);
this.newParentId = json['newParentId'];
this.newInputName = json['newInputName'];
@@ -124,19 +114,27 @@ export class BlockMove extends BlockBase {
*/
private currentLocation_(): BlockLocation {
const workspace = this.getEventWorkspace_();
if (!this.blockId) {
throw new Error(
'The block ID is undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
const block = workspace.getBlockById(this.blockId);
if (!block) {
throw new Error(
'The block associated with the block move event ' +
'could not be found');
}
const location = {} as BlockLocation;
const parent = block!.getParent();
const parent = block.getParent();
if (parent) {
location.parentId = parent.id;
// AnyDuringMigration because: Argument of type 'Block | null' is not
// assignable to parameter of type 'Block'.
const input = parent.getInputWithBlock(block as AnyDuringMigration);
const input = parent.getInputWithBlock(block);
if (input) {
location.inputName = input.name;
}
} else {
location.coordinate = block!.getRelativeToSurfaceXY();
location.coordinate = block.getRelativeToSurfaceXY();
}
return location;
}
@@ -159,6 +157,11 @@ export class BlockMove extends BlockBase {
*/
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (!this.blockId) {
throw new Error(
'The block ID is undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
const block = workspace.getBlockById(this.blockId);
if (!block) {
console.warn('Can\'t move non-existent block: ' + this.blockId);
@@ -206,4 +209,11 @@ export class BlockMove extends BlockBase {
}
}
export interface BlockMoveJson extends BlockBaseJson {
newParentId?: string;
newInputName?: string;
newCoordinate?: string;
recordUndo?: boolean;
}
registry.register(registry.Type.EVENT, eventUtils.MOVE, BlockMove);

View File

@@ -12,9 +12,9 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BubbleOpen');
import type {AbstractEventJson} from './events_abstract.js';
import type {BlockSvg} from '../block_svg.js';
import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
@@ -25,10 +25,10 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.BubbleOpen
*/
export class BubbleOpen extends UiBase {
blockId: string|null;
blockId?: string;
isOpen?: boolean;
bubbleType?: string;
override type: string;
bubbleType?: BubbleType;
override type = eventUtils.BUBBLE_OPEN;
/**
* @param opt_block The associated block. Undefined for a blank event.
@@ -38,19 +38,18 @@ export class BubbleOpen extends UiBase {
* 'warning'. Undefined for a blank event.
*/
constructor(
opt_block?: BlockSvg, opt_isOpen?: boolean, opt_bubbleType?: string) {
opt_block?: BlockSvg, opt_isOpen?: boolean, opt_bubbleType?: BubbleType) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined;
super(workspaceId);
this.blockId = opt_block ? opt_block.id : null;
if (!opt_block) return;
this.blockId = opt_block.id;
/** Whether the bubble is opening (false if closing). */
this.isOpen = opt_isOpen;
/** The type of bubble. One of 'mutator', 'comment', or 'warning'. */
this.bubbleType = opt_bubbleType;
/** Type of this event. */
this.type = eventUtils.BUBBLE_OPEN;
}
/**
@@ -58,11 +57,21 @@ export class BubbleOpen extends UiBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): BubbleOpenJson {
const json = super.toJson() as BubbleOpenJson;
if (this.isOpen === undefined) {
throw new Error(
'Whether this event is for opening the bubble is undefined. ' +
'Either pass the value to the constructor, or call fromJson');
}
if (!this.bubbleType) {
throw new Error(
'The type of bubble is undefined. Either pass the ' +
'value to the constructor, or call fromJson');
}
json['isOpen'] = this.isOpen;
json['bubbleType'] = this.bubbleType;
json['blockId'] = this.blockId;
json['blockId'] = this.blockId || '';
return json;
}
@@ -71,7 +80,7 @@ export class BubbleOpen extends UiBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: BubbleOpenJson) {
super.fromJson(json);
this.isOpen = json['isOpen'];
this.bubbleType = json['bubbleType'];
@@ -79,4 +88,16 @@ export class BubbleOpen extends UiBase {
}
}
export enum BubbleType {
MUTATOR = 'mutator',
COMMENT = 'comment',
WARNING = 'warning',
}
export interface BubbleOpenJson extends AbstractEventJson {
isOpen: boolean;
bubbleType: BubbleType;
blockId: string;
}
registry.register(registry.Type.EVENT, eventUtils.BUBBLE_OPEN, BubbleOpen);

View File

@@ -14,6 +14,7 @@ goog.declareModuleId('Blockly.Events.Click');
import type {Block} from '../block.js';
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
@@ -25,9 +26,9 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.Click
*/
export class Click extends UiBase {
blockId: AnyDuringMigration;
targetType?: string;
override type: string;
blockId?: string;
targetType?: ClickTarget;
override type = eventUtils.CLICK;
/**
* @param opt_block The affected block. Null for click events that do not have
@@ -40,19 +41,17 @@ export class Click extends UiBase {
*/
constructor(
opt_block?: Block|null, opt_workspaceId?: string|null,
opt_targetType?: string) {
opt_targetType?: ClickTarget) {
let workspaceId = opt_block ? opt_block.workspace.id : opt_workspaceId;
if (workspaceId === null) {
workspaceId = undefined;
}
super(workspaceId);
this.blockId = opt_block ? opt_block.id : null;
this.blockId = opt_block ? opt_block.id : undefined;
/** The type of element targeted by this click event. */
this.targetType = opt_targetType;
/** Type of this event. */
this.type = eventUtils.CLICK;
}
/**
@@ -60,12 +59,15 @@ export class Click extends UiBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['targetType'] = this.targetType;
if (this.blockId) {
json['blockId'] = this.blockId;
override toJson(): ClickJson {
const json = super.toJson() as ClickJson;
if (!this.targetType) {
throw new Error(
'The click target type is undefined. Either pass a block to ' +
'the constructor, or call fromJson');
}
json['targetType'] = this.targetType;
json['blockId'] = this.blockId;
return json;
}
@@ -74,11 +76,22 @@ export class Click extends UiBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: ClickJson) {
super.fromJson(json);
this.targetType = json['targetType'];
this.blockId = json['blockId'];
}
}
export enum ClickTarget {
BLOCK = 'block',
WORKSPACE = 'workspace',
ZOOM_CONTROLS = 'zoom_controls',
}
export interface ClickJson extends AbstractEventJson {
targetType: ClickTarget;
blockId?: string;
}
registry.register(registry.Type.EVENT, eventUtils.CLICK, Click);

View File

@@ -16,7 +16,7 @@ import * as utilsXml from '../utils/xml.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import * as Xml from '../xml.js';
import {Abstract as AbstractEvent} from './events_abstract.js';
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js';
import type {CommentCreate} from './events_comment_create.js';
import type {CommentDelete} from './events_comment_delete.js';
import * as eventUtils from './utils.js';
@@ -28,9 +28,8 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.CommentBase
*/
export class CommentBase extends AbstractEvent {
override isBlank: boolean;
commentId: string;
override workspaceId: string;
override isBlank = true;
commentId?: string;
/**
* @param opt_comment The comment this event corresponds to. Undefined for a
@@ -39,13 +38,15 @@ export class CommentBase extends AbstractEvent {
constructor(opt_comment?: WorkspaceComment) {
super();
/** Whether or not an event is blank. */
this.isBlank = typeof opt_comment === 'undefined';
this.isBlank = !opt_comment;
if (!opt_comment) return;
/** The ID of the comment this event pertains to. */
this.commentId = this.isBlank ? '' : opt_comment!.id;
this.commentId = opt_comment.id;
/** The workspace identifier for this event. */
this.workspaceId = this.isBlank ? '' : opt_comment!.workspace.id;
this.workspaceId = opt_comment.workspace.id;
/**
* The event group ID for the group this event belongs to. Groups define
@@ -63,11 +64,14 @@ export class CommentBase extends AbstractEvent {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
if (this.commentId) {
json['commentId'] = this.commentId;
override toJson(): CommentBaseJson {
const json = super.toJson() as CommentBaseJson;
if (!this.commentId) {
throw new Error(
'The comment ID is undefined. Either pass a comment to ' +
'the constructor, or call fromJson');
}
json['commentId'] = this.commentId;
return json;
}
@@ -76,7 +80,7 @@ export class CommentBase extends AbstractEvent {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: CommentBaseJson) {
super.fromJson(json);
this.commentId = json['commentId'];
}
@@ -92,9 +96,17 @@ export class CommentBase extends AbstractEvent {
const workspace = event.getEventWorkspace_();
if (create) {
const xmlElement = utilsXml.createElement('xml');
if (!event.xml) {
throw new Error('Ecountered a comment event without proper xml');
}
xmlElement.appendChild(event.xml);
Xml.domToWorkspace(xmlElement, workspace);
} else {
if (!event.commentId) {
throw new Error(
'The comment ID is undefined. Either pass a comment to ' +
'the constructor, or call fromJson');
}
const comment = workspace.getCommentById(event.commentId);
if (comment) {
comment.dispose();
@@ -106,3 +118,7 @@ export class CommentBase extends AbstractEvent {
}
}
}
export interface CommentBaseJson extends AbstractEventJson {
commentId: string;
}

View File

@@ -15,7 +15,7 @@ goog.declareModuleId('Blockly.Events.CommentChange');
import * as registry from '../registry.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import {CommentBase} from './events_comment_base.js';
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
import * as eventUtils from './utils.js';
@@ -25,12 +25,9 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.CommentChange
*/
export class CommentChange extends CommentBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldContents_!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
newContents_!: string;
override type = eventUtils.COMMENT_CHANGE;
oldContents_?: string;
newContents_?: string;
/**
* @param opt_comment The comment that is being changed. Undefined for a
@@ -43,9 +40,6 @@ export class CommentChange extends CommentBase {
opt_newContents?: string) {
super(opt_comment);
/** Type of this event. */
this.type = eventUtils.COMMENT_CHANGE;
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
}
@@ -61,8 +55,18 @@ export class CommentChange extends CommentBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): CommentChangeJson {
const json = super.toJson() as CommentChangeJson;
if (!this.oldContents_) {
throw new Error(
'The old contents is undefined. Either pass a value to ' +
'the constructor, or call fromJson');
}
if (!this.newContents_) {
throw new Error(
'The new contents is undefined. Either pass a value to ' +
'the constructor, or call fromJson');
}
json['oldContents'] = this.oldContents_;
json['newContents'] = this.newContents_;
return json;
@@ -73,7 +77,7 @@ export class CommentChange extends CommentBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: CommentChangeJson) {
super.fromJson(json);
this.oldContents_ = json['oldContents'];
this.newContents_ = json['newContents'];
@@ -95,16 +99,35 @@ export class CommentChange extends CommentBase {
*/
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (!this.commentId) {
throw new Error(
'The comment ID is undefined. Either pass a comment to ' +
'the constructor, or call fromJson');
}
const comment = workspace.getCommentById(this.commentId);
if (!comment) {
console.warn('Can\'t change non-existent comment: ' + this.commentId);
return;
}
const contents = forward ? this.newContents_ : this.oldContents_;
if (!contents) {
if (forward) {
throw new Error(
'The new contents is undefined. Either pass a value to ' +
'the constructor, or call fromJson');
}
throw new Error(
'The old contents is undefined. Either pass a value to ' +
'the constructor, or call fromJson');
}
comment.setContent(contents);
}
}
export interface CommentChangeJson extends CommentBaseJson {
oldContents: string;
newContents: string;
}
registry.register(
registry.Type.EVENT, eventUtils.COMMENT_CHANGE, CommentChange);

View File

@@ -16,7 +16,7 @@ import * as registry from '../registry.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import * as Xml from '../xml.js';
import {CommentBase} from './events_comment_base.js';
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
import * as eventUtils from './utils.js';
@@ -26,9 +26,9 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.CommentCreate
*/
export class CommentCreate extends CommentBase {
override type: string;
override type = eventUtils.COMMENT_CREATE;
xml: AnyDuringMigration;
xml?: Element|DocumentFragment;
/**
* @param opt_comment The created comment.
@@ -37,9 +37,6 @@ export class CommentCreate extends CommentBase {
constructor(opt_comment?: WorkspaceComment) {
super(opt_comment);
/** Type of this event. */
this.type = eventUtils.COMMENT_CREATE;
if (!opt_comment) {
return;
}
@@ -53,8 +50,13 @@ export class CommentCreate extends CommentBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): CommentCreateJson {
const json = super.toJson() as CommentCreateJson;
if (!this.xml) {
throw new Error(
'The comment XML is undefined. Either pass a comment to ' +
'the constructor, or call fromJson');
}
json['xml'] = Xml.domToText(this.xml);
return json;
}
@@ -64,7 +66,7 @@ export class CommentCreate extends CommentBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: CommentCreateJson) {
super.fromJson(json);
this.xml = Xml.textToDom(json['xml']);
}
@@ -79,5 +81,9 @@ export class CommentCreate extends CommentBase {
}
}
export interface CommentCreateJson extends CommentBaseJson {
xml: string;
}
registry.register(
registry.Type.EVENT, eventUtils.COMMENT_CREATE, CommentCreate);

View File

@@ -25,8 +25,8 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.CommentDelete
*/
export class CommentDelete extends CommentBase {
override type: string;
xml: AnyDuringMigration;
override type = eventUtils.COMMENT_DELETE;
xml?: Element;
/**
* @param opt_comment The deleted comment.
@@ -35,34 +35,12 @@ export class CommentDelete extends CommentBase {
constructor(opt_comment?: WorkspaceComment) {
super(opt_comment);
/** Type of this event. */
this.type = eventUtils.COMMENT_DELETE;
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
}
this.xml = opt_comment.toXmlWithXY();
}
// TODO (#1266): "Full" and "minimal" serialization.
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
return json;
}
/**
* Decode the JSON event.
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
}
/**
* Run a creation event.

View File

@@ -16,7 +16,7 @@ import * as registry from '../registry.js';
import {Coordinate} from '../utils/coordinate.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import {CommentBase} from './events_comment_base.js';
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
import * as eventUtils from './utils.js';
@@ -26,17 +26,11 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.CommentMove
*/
export class CommentMove extends CommentBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
comment_!: WorkspaceComment;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldCoordinate_!: Coordinate;
override type = eventUtils.COMMENT_MOVE;
comment_?: WorkspaceComment;
oldCoordinate_?: Coordinate;
/** The location after the move, in workspace coordinates. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Coordinate'.
newCoordinate_: Coordinate = null as AnyDuringMigration;
newCoordinate_?: Coordinate;
/**
* @param opt_comment The comment that is being moved. Undefined for a blank
@@ -45,16 +39,12 @@ export class CommentMove extends CommentBase {
constructor(opt_comment?: WorkspaceComment) {
super(opt_comment);
/** Type of this event. */
this.type = eventUtils.COMMENT_MOVE;
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
}
/**
* The comment that is being moved. Will be cleared after recording the new
* location.
* The comment that is being moved.
*/
this.comment_ = opt_comment;
@@ -67,15 +57,17 @@ export class CommentMove extends CommentBase {
* called once.
*/
recordNew() {
if (!this.comment_) {
if (this.newCoordinate_) {
throw Error(
'Tried to record the new position of a comment on the ' +
'same event twice.');
}
if (!this.comment_) {
throw new Error(
'The comment is undefined. Pass a comment to ' +
'the constructor if you want to use the record functionality');
}
this.newCoordinate_ = this.comment_.getXY();
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'WorkspaceComment'.
this.comment_ = null as AnyDuringMigration;
}
/**
@@ -94,16 +86,22 @@ export class CommentMove extends CommentBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
if (this.oldCoordinate_) {
json['oldCoordinate'] = Math.round(this.oldCoordinate_.x) + ',' +
Math.round(this.oldCoordinate_.y);
override toJson(): CommentMoveJson {
const json = super.toJson() as CommentMoveJson;
if (!this.oldCoordinate_) {
throw new Error(
'The old comment position is undefined. Either pass a comment to ' +
'the constructor, or call fromJson');
}
if (this.newCoordinate_) {
json['newCoordinate'] = Math.round(this.newCoordinate_.x) + ',' +
Math.round(this.newCoordinate_.y);
if (!this.newCoordinate_) {
throw new Error(
'The new comment position is undefined. Either call recordNew, or ' +
'call fromJson');
}
json['oldCoordinate'] = `${Math.round(this.oldCoordinate_.x)}, ` +
`${Math.round(this.oldCoordinate_.y)}`;
json['newCoordinate'] = Math.round(this.newCoordinate_.x) + ',' +
Math.round(this.newCoordinate_.y);
return json;
}
@@ -112,17 +110,12 @@ export class CommentMove extends CommentBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: CommentMoveJson) {
super.fromJson(json);
if (json['oldCoordinate']) {
const xy = json['oldCoordinate'].split(',');
this.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
}
if (json['newCoordinate']) {
const xy = json['newCoordinate'].split(',');
this.newCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
}
let xy = json['oldCoordinate'].split(',');
this.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
xy = json['newCoordinate'].split(',');
this.newCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
}
/**
@@ -141,6 +134,11 @@ export class CommentMove extends CommentBase {
*/
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (!this.commentId) {
throw new Error(
'The comment ID is undefined. Either pass a comment to ' +
'the constructor, or call fromJson');
}
const comment = workspace.getCommentById(this.commentId);
if (!comment) {
console.warn('Can\'t move non-existent comment: ' + this.commentId);
@@ -148,10 +146,21 @@ export class CommentMove extends CommentBase {
}
const target = forward ? this.newCoordinate_ : this.oldCoordinate_;
if (!target) {
throw new Error(
'Either oldCoordinate_ or newCoordinate_ is undefined. ' +
'Either pass a comment to the constructor and call recordNew, ' +
'or call fromJson');
}
// TODO: Check if the comment is being dragged, and give up if so.
const current = comment.getXY();
comment.moveBy(target.x - current.x, target.y - current.y);
}
}
export interface CommentMoveJson extends CommentBaseJson {
oldCoordinate: string;
newCoordinate: string;
}
registry.register(registry.Type.EVENT, eventUtils.COMMENT_MOVE, CommentMove);

View File

@@ -16,6 +16,7 @@ import type {Block} from '../block.js';
import {ASTNode} from '../keyboard_nav/ast_node.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {AbstractEventJson} from './events_abstract.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
@@ -27,11 +28,11 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.MarkerMove
*/
export class MarkerMove extends UiBase {
blockId: string|null;
oldNode?: ASTNode|null;
blockId?: string;
oldNode?: ASTNode;
newNode?: ASTNode;
isCursor?: boolean;
override type: string;
override type = eventUtils.MARKER_MOVE;
/**
* @param opt_block The affected block. Null if current node is of type
@@ -52,20 +53,17 @@ export class MarkerMove extends UiBase {
}
super(workspaceId);
/** The workspace identifier for this event. */
this.blockId = opt_block ? opt_block.id : null;
/** The block identifier for this event. */
this.blockId = opt_block?.id;
/** The old node the marker used to be on. */
this.oldNode = opt_oldNode;
this.oldNode = opt_oldNode || undefined;
/** The new node the marker is now on. */
this.newNode = opt_newNode;
/** Whether this is a cursor event. */
this.isCursor = isCursor;
/** Type of this event. */
this.type = eventUtils.MARKER_MOVE;
}
/**
@@ -73,8 +71,18 @@ export class MarkerMove extends UiBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): MarkerMoveJson {
const json = super.toJson() as MarkerMoveJson;
if (this.isCursor === undefined) {
throw new Error(
'Whether this is a cursor event or not is undefined. Either pass ' +
'a value to the constructor, or call fromJson');
}
if (!this.newNode) {
throw new Error(
'The new node is undefined. Either pass a node to ' +
'the constructor, or call fromJson');
}
json['isCursor'] = this.isCursor;
json['blockId'] = this.blockId;
json['oldNode'] = this.oldNode;
@@ -87,7 +95,7 @@ export class MarkerMove extends UiBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: MarkerMoveJson) {
super.fromJson(json);
this.isCursor = json['isCursor'];
this.blockId = json['blockId'];
@@ -96,4 +104,11 @@ export class MarkerMove extends UiBase {
}
}
export interface MarkerMoveJson extends AbstractEventJson {
isCursor: boolean;
blockId?: string;
oldNode?: ASTNode;
newNode: ASTNode;
}
registry.register(registry.Type.EVENT, eventUtils.MARKER_MOVE, MarkerMove);

View File

@@ -13,6 +13,7 @@ import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.Selected');
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
@@ -24,9 +25,9 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.Selected
*/
export class Selected extends UiBase {
oldElementId?: string|null;
newElementId?: string|null;
override type: string;
oldElementId?: string;
newElementId?: string;
override type = eventUtils.SELECTED;
/**
* @param opt_oldElementId The ID of the previously selected element. Null if
@@ -41,14 +42,11 @@ export class Selected extends UiBase {
opt_workspaceId?: string) {
super(opt_workspaceId);
/** The ID of the last selected element. */
this.oldElementId = opt_oldElementId;
/** The id of the last selected element. */
this.oldElementId = opt_oldElementId ?? undefined;
/** The ID of the selected element. */
this.newElementId = opt_newElementId;
/** Type of this event. */
this.type = eventUtils.SELECTED;
/** The id of the selected element. */
this.newElementId = opt_newElementId ?? undefined;
}
/**
@@ -56,8 +54,8 @@ export class Selected extends UiBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): SelectedJson {
const json = super.toJson() as SelectedJson;
json['oldElementId'] = this.oldElementId;
json['newElementId'] = this.newElementId;
return json;
@@ -68,11 +66,16 @@ export class Selected extends UiBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: SelectedJson) {
super.fromJson(json);
this.oldElementId = json['oldElementId'];
this.newElementId = json['newElementId'];
}
}
export interface SelectedJson extends AbstractEventJson {
oldElementId?: string;
newElementId?: string;
}
registry.register(registry.Type.EVENT, eventUtils.SELECTED, Selected);

View File

@@ -13,7 +13,7 @@ import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.ThemeChange');
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
@@ -25,7 +25,7 @@ import * as eventUtils from './utils.js';
*/
export class ThemeChange extends UiBase {
themeName?: string;
override type: string;
override type = eventUtils.THEME_CHANGE;
/**
* @param opt_themeName The theme name. Undefined for a blank event.
@@ -37,9 +37,6 @@ export class ThemeChange extends UiBase {
/** The theme name. */
this.themeName = opt_themeName;
/** Type of this event. */
this.type = eventUtils.THEME_CHANGE;
}
/**
@@ -47,8 +44,13 @@ export class ThemeChange extends UiBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): ThemeChangeJson {
const json = super.toJson() as ThemeChangeJson;
if (!this.themeName) {
throw new Error(
'The theme name is undefined. Either pass a theme name to ' +
'the constructor, or call fromJson');
}
json['themeName'] = this.themeName;
return json;
}
@@ -58,10 +60,14 @@ export class ThemeChange extends UiBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: ThemeChangeJson) {
super.fromJson(json);
this.themeName = json['themeName'];
}
}
export interface ThemeChangeJson extends AbstractEventJson {
themeName: string;
}
registry.register(registry.Type.EVENT, eventUtils.THEME_CHANGE, ThemeChange);

View File

@@ -13,7 +13,7 @@ import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.ToolboxItemSelect');
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
@@ -24,9 +24,9 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.ToolboxItemSelect
*/
export class ToolboxItemSelect extends UiBase {
oldItem?: string|null;
newItem?: string|null;
override type: string;
oldItem?: string;
newItem?: string;
override type = eventUtils.TOOLBOX_ITEM_SELECT;
/**
* @param opt_oldItem The previously selected toolbox item.
@@ -42,13 +42,10 @@ export class ToolboxItemSelect extends UiBase {
super(opt_workspaceId);
/** The previously selected toolbox item. */
this.oldItem = opt_oldItem;
this.oldItem = opt_oldItem ?? undefined;
/** The newly selected toolbox item. */
this.newItem = opt_newItem;
/** Type of this event. */
this.type = eventUtils.TOOLBOX_ITEM_SELECT;
this.newItem = opt_newItem ?? undefined;
}
/**
@@ -56,8 +53,8 @@ export class ToolboxItemSelect extends UiBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): ToolboxItemSelectJson {
const json = super.toJson() as ToolboxItemSelectJson;
json['oldItem'] = this.oldItem;
json['newItem'] = this.newItem;
return json;
@@ -68,12 +65,17 @@ export class ToolboxItemSelect extends UiBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: ToolboxItemSelectJson) {
super.fromJson(json);
this.oldItem = json['oldItem'];
this.newItem = json['newItem'];
}
}
export interface ToolboxItemSelectJson extends AbstractEventJson {
oldItem?: string;
newItem?: string;
}
registry.register(
registry.Type.EVENT, eventUtils.TOOLBOX_ITEM_SELECT, ToolboxItemSelect);

View File

@@ -13,6 +13,7 @@ import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.TrashcanOpen');
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
@@ -25,7 +26,7 @@ import * as eventUtils from './utils.js';
*/
export class TrashcanOpen extends UiBase {
isOpen?: boolean;
override type: string;
override type = eventUtils.TRASHCAN_OPEN;
/**
* @param opt_isOpen Whether the trashcan flyout is opening (false if
@@ -38,9 +39,6 @@ export class TrashcanOpen extends UiBase {
/** Whether the trashcan flyout is opening (false if closing). */
this.isOpen = opt_isOpen;
/** Type of this event. */
this.type = eventUtils.TRASHCAN_OPEN;
}
/**
@@ -48,8 +46,13 @@ export class TrashcanOpen extends UiBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): TrashcanOpenJson {
const json = super.toJson() as TrashcanOpenJson;
if (this.isOpen === undefined) {
throw new Error(
'Whether this is already open or not is undefined. Either pass ' +
'a value to the constructor, or call fromJson');
}
json['isOpen'] = this.isOpen;
return json;
}
@@ -59,10 +62,14 @@ export class TrashcanOpen extends UiBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: TrashcanOpenJson) {
super.fromJson(json);
this.isOpen = json['isOpen'];
}
}
export interface TrashcanOpenJson extends AbstractEventJson {
isOpen: boolean;
}
registry.register(registry.Type.EVENT, eventUtils.TRASHCAN_OPEN, TrashcanOpen);

View File

@@ -15,7 +15,6 @@ goog.declareModuleId('Blockly.Events.Ui');
import type {Block} from '../block.js';
import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
@@ -31,7 +30,7 @@ export class Ui extends UiBase {
element: AnyDuringMigration;
oldValue: AnyDuringMigration;
newValue: AnyDuringMigration;
override type: string;
override type = eventUtils.UI;
/**
* @param opt_block The affected block. Null for UI events that do not have
@@ -50,9 +49,6 @@ export class Ui extends UiBase {
this.element = typeof opt_element === 'undefined' ? '' : opt_element;
this.oldValue = typeof opt_oldValue === 'undefined' ? '' : opt_oldValue;
this.newValue = typeof opt_newValue === 'undefined' ? '' : opt_newValue;
/** Type of this event. */
this.type = eventUtils.UI;
}
/**
@@ -61,7 +57,7 @@ export class Ui extends UiBase {
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
const json = super.toJson() as AnyDuringMigration;
json['element'] = this.element;
if (this.newValue !== undefined) {
json['newValue'] = this.newValue;

View File

@@ -26,7 +26,7 @@ import {Abstract as AbstractEvent} from './events_abstract.js';
* @alias Blockly.Events.UiBase
*/
export class UiBase extends AbstractEvent {
override isBlank: boolean;
override isBlank = true;
override workspaceId: string;
// UI events do not undo or redo.

View File

@@ -14,7 +14,7 @@ goog.declareModuleId('Blockly.Events.VarBase');
import type {VariableModel} from '../variable_model.js';
import {Abstract as AbstractEvent} from './events_abstract.js';
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js';
/**
@@ -23,9 +23,8 @@ import {Abstract as AbstractEvent} from './events_abstract.js';
* @alias Blockly.Events.VarBase
*/
export class VarBase extends AbstractEvent {
override isBlank: AnyDuringMigration;
varId: string;
override workspaceId: string;
override isBlank = true;
varId?: string;
/**
* @param opt_variable The variable this event corresponds to. Undefined for
@@ -34,12 +33,13 @@ export class VarBase extends AbstractEvent {
constructor(opt_variable?: VariableModel) {
super();
this.isBlank = typeof opt_variable === 'undefined';
if (!opt_variable) return;
/** The variable ID for the variable this event pertains to. */
this.varId = this.isBlank ? '' : opt_variable!.getId();
/** The variable id for the variable this event pertains to. */
this.varId = opt_variable.getId();
/** The workspace identifier for this event. */
this.workspaceId = this.isBlank ? '' : opt_variable!.workspace.id;
this.workspaceId = opt_variable.workspace.id;
}
/**
@@ -47,8 +47,13 @@ export class VarBase extends AbstractEvent {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): VarBaseJson {
const json = super.toJson() as VarBaseJson;
if (!this.varId) {
throw new Error(
'The var ID is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');
}
json['varId'] = this.varId;
return json;
}
@@ -58,8 +63,12 @@ export class VarBase extends AbstractEvent {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: VarBaseJson) {
super.fromJson(json);
this.varId = json['varId'];
}
}
export interface VarBaseJson extends AbstractEventJson {
varId: string;
}

View File

@@ -15,7 +15,7 @@ goog.declareModuleId('Blockly.Events.VarCreate');
import * as registry from '../registry.js';
import type {VariableModel} from '../variable_model.js';
import {VarBase} from './events_var_base.js';
import {VarBase, VarBaseJson} from './events_var_base.js';
import * as eventUtils from './utils.js';
@@ -25,12 +25,9 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.VarCreate
*/
export class VarCreate extends VarBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
varType!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
varName!: string;
override type = eventUtils.VAR_CREATE;
varType?: string;
varName?: string;
/**
* @param opt_variable The created variable. Undefined for a blank event.
@@ -38,9 +35,6 @@ export class VarCreate extends VarBase {
constructor(opt_variable?: VariableModel) {
super(opt_variable);
/** Type of this event. */
this.type = eventUtils.VAR_CREATE;
if (!opt_variable) {
return; // Blank event to be populated by fromJson.
}
@@ -53,8 +47,18 @@ export class VarCreate extends VarBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): VarCreateJson {
const json = super.toJson() as VarCreateJson;
if (!this.varType) {
throw new Error(
'The var type is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');
}
if (!this.varName) {
throw new Error(
'The var name is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');
}
json['varType'] = this.varType;
json['varName'] = this.varName;
return json;
@@ -65,7 +69,7 @@ export class VarCreate extends VarBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: VarCreateJson) {
super.fromJson(json);
this.varType = json['varType'];
this.varName = json['varName'];
@@ -78,6 +82,16 @@ export class VarCreate extends VarBase {
*/
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (!this.varId) {
throw new Error(
'The var ID is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');
}
if (!this.varName) {
throw new Error(
'The var name is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');
}
if (forward) {
workspace.createVariable(this.varName, this.varType, this.varId);
} else {
@@ -86,4 +100,9 @@ export class VarCreate extends VarBase {
}
}
export interface VarCreateJson extends VarBaseJson {
varType: string;
varName: string;
}
registry.register(registry.Type.EVENT, eventUtils.VAR_CREATE, VarCreate);

View File

@@ -15,7 +15,7 @@ goog.declareModuleId('Blockly.Events.VarDelete');
import * as registry from '../registry.js';
import type {VariableModel} from '../variable_model.js';
import {VarBase} from './events_var_base.js';
import {VarBase, VarBaseJson} from './events_var_base.js';
import * as eventUtils from './utils.js';
@@ -25,12 +25,9 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.VarDelete
*/
export class VarDelete extends VarBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
varType!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
varName!: string;
override type = eventUtils.VAR_DELETE;
varType?: string;
varName?: string;
/**
* @param opt_variable The deleted variable. Undefined for a blank event.
@@ -38,9 +35,6 @@ export class VarDelete extends VarBase {
constructor(opt_variable?: VariableModel) {
super(opt_variable);
/** Type of this event. */
this.type = eventUtils.VAR_DELETE;
if (!opt_variable) {
return; // Blank event to be populated by fromJson.
}
@@ -53,8 +47,18 @@ export class VarDelete extends VarBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): VarDeleteJson {
const json = super.toJson() as VarDeleteJson;
if (!this.varType) {
throw new Error(
'The var type is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');
}
if (!this.varName) {
throw new Error(
'The var name is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');
}
json['varType'] = this.varType;
json['varName'] = this.varName;
return json;
@@ -65,7 +69,7 @@ export class VarDelete extends VarBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: VarDeleteJson) {
super.fromJson(json);
this.varType = json['varType'];
this.varName = json['varName'];
@@ -78,6 +82,16 @@ export class VarDelete extends VarBase {
*/
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (!this.varId) {
throw new Error(
'The var ID is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');
}
if (!this.varName) {
throw new Error(
'The var name is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');
}
if (forward) {
workspace.deleteVariableById(this.varId);
} else {
@@ -86,4 +100,9 @@ export class VarDelete extends VarBase {
}
}
export interface VarDeleteJson extends VarBaseJson {
varType: string;
varName: string;
}
registry.register(registry.Type.EVENT, eventUtils.VAR_DELETE, VarDelete);

View File

@@ -15,7 +15,7 @@ goog.declareModuleId('Blockly.Events.VarRename');
import * as registry from '../registry.js';
import type {VariableModel} from '../variable_model.js';
import {VarBase} from './events_var_base.js';
import {VarBase, VarBaseJson} from './events_var_base.js';
import * as eventUtils from './utils.js';
@@ -25,12 +25,9 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.VarRename
*/
export class VarRename extends VarBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldName!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
newName!: string;
override type = eventUtils.VAR_RENAME;
oldName?: string;
newName?: string;
/**
* @param opt_variable The renamed variable. Undefined for a blank event.
@@ -39,9 +36,6 @@ export class VarRename extends VarBase {
constructor(opt_variable?: VariableModel, newName?: string) {
super(opt_variable);
/** Type of this event. */
this.type = eventUtils.VAR_RENAME;
if (!opt_variable) {
return; // Blank event to be populated by fromJson.
}
@@ -54,8 +48,18 @@ export class VarRename extends VarBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): VarRenameJson {
const json = super.toJson() as VarRenameJson;
if (!this.oldName) {
throw new Error(
'The old var name is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');
}
if (!this.newName) {
throw new Error(
'The new var name is undefined. Either pass a value to ' +
'the constructor, or call fromJson');
}
json['oldName'] = this.oldName;
json['newName'] = this.newName;
return json;
@@ -66,7 +70,7 @@ export class VarRename extends VarBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: VarRenameJson) {
super.fromJson(json);
this.oldName = json['oldName'];
this.newName = json['newName'];
@@ -79,6 +83,21 @@ export class VarRename extends VarBase {
*/
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (!this.varId) {
throw new Error(
'The var ID is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');
}
if (!this.oldName) {
throw new Error(
'The old var name is undefined. Either pass a variable to ' +
'the constructor, or call fromJson');
}
if (!this.newName) {
throw new Error(
'The new var name is undefined. Either pass a value to ' +
'the constructor, or call fromJson');
}
if (forward) {
workspace.renameVariableById(this.varId, this.newName);
} else {
@@ -87,4 +106,9 @@ export class VarRename extends VarBase {
}
}
export interface VarRenameJson extends VarBaseJson {
oldName: string;
newName: string;
}
registry.register(registry.Type.EVENT, eventUtils.VAR_RENAME, VarRename);

View File

@@ -13,7 +13,7 @@ import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.ViewportChange');
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
@@ -28,7 +28,7 @@ export class ViewportChange extends UiBase {
viewLeft?: number;
scale?: number;
oldScale?: number;
override type: string;
override type = eventUtils.VIEWPORT_CHANGE;
/**
* @param opt_top Top-edge of the visible portion of the workspace, relative
@@ -63,9 +63,6 @@ export class ViewportChange extends UiBase {
/** The old scale of the workspace. */
this.oldScale = opt_oldScale;
/** Type of this event. */
this.type = eventUtils.VIEWPORT_CHANGE;
}
/**
@@ -73,8 +70,28 @@ export class ViewportChange extends UiBase {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = super.toJson();
override toJson(): ViewportChangeJson {
const json = super.toJson() as ViewportChangeJson;
if (this.viewTop === undefined) {
throw new Error(
'The view top is undefined. Either pass a value to ' +
'the constructor, or call fromJson');
}
if (this.viewLeft === undefined) {
throw new Error(
'The view left is undefined. Either pass a value to ' +
'the constructor, or call fromJson');
}
if (this.scale === undefined) {
throw new Error(
'The scale is undefined. Either pass a value to ' +
'the constructor, or call fromJson');
}
if (this.oldScale === undefined) {
throw new Error(
'The old scale is undefined. Either pass a value to ' +
'the constructor, or call fromJson');
}
json['viewTop'] = this.viewTop;
json['viewLeft'] = this.viewLeft;
json['scale'] = this.scale;
@@ -87,7 +104,7 @@ export class ViewportChange extends UiBase {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
override fromJson(json: ViewportChangeJson) {
super.fromJson(json);
this.viewTop = json['viewTop'];
this.viewLeft = json['viewLeft'];
@@ -96,5 +113,12 @@ export class ViewportChange extends UiBase {
}
}
export interface ViewportChangeJson extends AbstractEventJson {
viewTop: number;
viewLeft: number;
scale: number;
oldScale: number;
}
registry.register(
registry.Type.EVENT, eventUtils.VIEWPORT_CHANGE, ViewportChange);

View File

@@ -536,6 +536,9 @@ export function disableOrphans(event: Abstract) {
}
const eventWorkspace =
common.getWorkspaceById(blockEvent.workspaceId) as WorkspaceSvg;
if (!blockEvent.blockId) {
throw new Error('Encountered a blockEvent without a proper blockId');
}
let block = eventWorkspace.getBlockById(blockEvent.blockId);
if (block) {
// Changing blocks as part of this event shouldn't be undoable.

View File

@@ -14,8 +14,7 @@ goog.declareModuleId('Blockly.Events.FinishedLoading');
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {Abstract as AbstractEvent} from './events_abstract.js';
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js';
import * as eventUtils from './utils.js';
@@ -28,13 +27,9 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.FinishedLoading
*/
export class FinishedLoading extends AbstractEvent {
override isBlank: boolean;
override workspaceId: string;
// Workspace events do not undo or redo.
override isBlank = true;
override recordUndo = false;
override type: string;
override group: AnyDuringMigration;
override type = eventUtils.FINISHED_LOADING;
/**
* @param opt_workspace The workspace that has finished loading. Undefined
@@ -43,13 +38,12 @@ export class FinishedLoading extends AbstractEvent {
constructor(opt_workspace?: Workspace) {
super();
/** Whether or not the event is blank (to be populated by fromJson). */
this.isBlank = typeof opt_workspace === 'undefined';
this.isBlank = !!opt_workspace;
if (!opt_workspace) return;
/** The workspace identifier for this event. */
this.workspaceId = opt_workspace ? opt_workspace.id : '';
/** Type of this event. */
this.type = eventUtils.FINISHED_LOADING;
this.workspaceId = opt_workspace.id;
}
/**
@@ -57,16 +51,14 @@ export class FinishedLoading extends AbstractEvent {
*
* @returns JSON representation.
*/
override toJson(): AnyDuringMigration {
const json = {
'type': this.type,
};
if (this.group) {
(json as AnyDuringMigration)['group'] = this.group;
}
if (this.workspaceId) {
(json as AnyDuringMigration)['workspaceId'] = this.workspaceId;
override toJson(): FinishedLoadingJson {
const json = super.toJson() as FinishedLoadingJson;
if (!this.workspaceId) {
throw new Error(
'The workspace ID is undefined. Either pass a workspace to ' +
'the constructor, or call fromJson');
}
json['workspaceId'] = this.workspaceId;
return json;
}
@@ -75,12 +67,15 @@ export class FinishedLoading extends AbstractEvent {
*
* @param json JSON representation.
*/
override fromJson(json: AnyDuringMigration) {
this.isBlank = false;
override fromJson(json: FinishedLoadingJson) {
super.fromJson(json);
this.workspaceId = json['workspaceId'];
this.group = json['group'];
}
}
export interface FinishedLoadingJson extends AbstractEventJson {
workspaceId: string;
}
registry.register(
registry.Type.EVENT, eventUtils.FINISHED_LOADING, FinishedLoading);

View File

@@ -98,7 +98,7 @@ export function registerMutator(
// Sanity checks passed.
register(name, function(this: Block) {
if (hasMutatorDialog) {
this.setMutator(new Mutator(this as BlockSvg, opt_blockList || []));
this.setMutator(new Mutator(opt_blockList || [], this as BlockSvg));
}
// Mixin the object.
this.mixin(mixinObj);

View File

@@ -54,9 +54,6 @@ import * as Xml from './xml.js';
export abstract class Field implements IASTNodeLocationSvg,
IASTNodeLocationWithBlock,
IKeyboardAccessible, IRegistrable {
/** The default value for this field. */
protected DEFAULT_VALUE: any = null;
/** Non-breaking space. */
static readonly NBSP = '\u00A0';
@@ -75,9 +72,7 @@ export abstract class Field implements IASTNodeLocationSvg,
protected value_: AnyDuringMigration;
/** Validation function called when user edits an editable field. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Function'.
protected validator_: Function = null as AnyDuringMigration;
protected validator_: Function|null = null;
/**
* Used to cache the field's tooltip value if setTooltip is called when the
@@ -90,44 +85,31 @@ export abstract class Field implements IASTNodeLocationSvg,
* Holds the cursors svg element when the cursor is attached to the field.
* This is null if there is no cursor on the field.
*/
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGElement'.
private cursorSvg_: SVGElement = null as AnyDuringMigration;
private cursorSvg_: SVGElement|null = null;
/**
* Holds the markers svg element when the marker is attached to the field.
* This is null if there is no marker on the field.
*/
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGElement'.
private markerSvg_: SVGElement = null as AnyDuringMigration;
private markerSvg_: SVGElement|null = null;
/** The rendered field's SVG group element. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGGElement'.
protected fieldGroup_: SVGGElement = null as AnyDuringMigration;
protected fieldGroup_: SVGGElement|null = null;
/** The rendered field's SVG border element. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGRectElement'.
protected borderRect_: SVGRectElement = null as AnyDuringMigration;
protected borderRect_: SVGRectElement|null = null;
/** The rendered field's SVG text element. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGTextElement'.
protected textElement_: SVGTextElement = null as AnyDuringMigration;
protected textElement_: SVGTextElement|null = null;
/** The rendered field's text content element. */
// AnyDuringMigration because: Type 'null' is not assignable to type 'Text'.
protected textContent_: Text = null as AnyDuringMigration;
protected textContent_: Text|null = null;
/** Mouse down event listener data. */
private mouseDownWrapper_: browserEvents.Data|null = null;
/** Constants associated with the source block's renderer. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'ConstantProvider'.
protected constants_: ConstantProvider = null as AnyDuringMigration;
protected constants_: ConstantProvider|null = null;
/**
* Has this field been disposed of?
@@ -140,8 +122,7 @@ export abstract class Field implements IASTNodeLocationSvg,
maxDisplayLength = 50;
/** Block this field is attached to. Starts as null, then set in init. */
// AnyDuringMigration because: Type 'null' is not assignable to type 'Block'.
protected sourceBlock_: Block = null as AnyDuringMigration;
protected sourceBlock_: Block|null = null;
/** Does this block need to be re-rendered? */
protected isDirty_ = true;
@@ -155,9 +136,7 @@ export abstract class Field implements IASTNodeLocationSvg,
protected enabled_ = true;
/** The element the click handler is bound to. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Element'.
protected clickTarget_: Element = null as AnyDuringMigration;
protected clickTarget_: Element|null = null;
/**
* The prefix field.
@@ -208,7 +187,9 @@ export abstract class Field implements IASTNodeLocationSvg,
* A generic value possessed by the field.
* Should generally be non-null, only null when the field is created.
*/
this.value_ = (new.target).prototype.DEFAULT_VALUE;
this.value_ = ('DEFAULT_VALUE' in (new.target).prototype) ?
((new.target).prototype as AnyDuringMigration).DEFAULT_VALUE :
null;
/** The size of the area rendered by the field. */
this.size_ = new Size(0, 0);
@@ -258,7 +239,8 @@ export abstract class Field implements IASTNodeLocationSvg,
* @returns The renderer constant provider.
*/
getConstants(): ConstantProvider|null {
if (!this.constants_ && this.sourceBlock_ && !this.sourceBlock_.disposed &&
if (!this.constants_ && this.sourceBlock_ &&
!this.sourceBlock_.isDeadOrDying() &&
this.sourceBlock_.workspace.rendered) {
this.constants_ = (this.sourceBlock_.workspace as WorkspaceSvg)
.getRenderer()
@@ -271,8 +253,9 @@ export abstract class Field implements IASTNodeLocationSvg,
* Get the block this field is attached to.
*
* @returns The block containing this field.
* @throws An error if the source block is not defined.
*/
getSourceBlock(): Block {
getSourceBlock(): Block|null {
return this.sourceBlock_;
}
@@ -361,9 +344,11 @@ export abstract class Field implements IASTNodeLocationSvg,
* do custom input handling.
*/
protected bindEvents_() {
Tooltip.bindMouseEvents(this.getClickTarget_());
const clickTarget = this.getClickTarget_();
if (!clickTarget) throw new Error('A click target has not been set.');
Tooltip.bindMouseEvents(clickTarget);
this.mouseDownWrapper_ = browserEvents.conditionalBind(
this.getClickTarget_(), 'mousedown', this, this.onMouseDown_);
clickTarget, 'mousedown', this, this.onMouseDown_);
}
/**
@@ -431,7 +416,7 @@ export abstract class Field implements IASTNodeLocationSvg,
* Used to see if `this` has overridden any relevant hooks.
* @returns The stringified version of the XML state, or null.
*/
protected saveLegacyState(callingClass: AnyDuringMigration): string|null {
protected saveLegacyState(callingClass: FieldProto): string|null {
if (callingClass.prototype.saveState === this.saveState &&
callingClass.prototype.toXml !== this.toXml) {
const elem = utilsXml.createElement('field');
@@ -454,7 +439,7 @@ export abstract class Field implements IASTNodeLocationSvg,
* @param state The state to apply to the field.
* @returns Whether the state was applied or not.
*/
loadLegacyState(callingClass: AnyDuringMigration, state: AnyDuringMigration):
loadLegacyState(callingClass: FieldProto, state: AnyDuringMigration):
boolean {
if (callingClass.prototype.loadState === this.loadState &&
callingClass.prototype.fromXml !== this.fromXml) {
@@ -488,16 +473,17 @@ export abstract class Field implements IASTNodeLocationSvg,
/** Add or remove the UI indicating if this field is editable or not. */
updateEditable() {
const group = this.fieldGroup_;
if (!this.EDITABLE || !group) {
const block = this.getSourceBlock();
if (!this.EDITABLE || !group || !block) {
return;
}
if (this.enabled_ && this.sourceBlock_.isEditable()) {
group.classList.add('blocklyEditableText');
group.classList.remove('blocklyNonEditableText');
if (this.enabled_ && block.isEditable()) {
dom.addClass(group, 'blocklyEditableText');
dom.removeClass(group, 'blocklyNonEditableText');
group.style.cursor = this.CURSOR;
} else {
group.classList.add('blocklyNonEditableText');
group.classList.remove('blocklyEditableText');
dom.addClass(group, 'blocklyNonEditableText');
dom.removeClass(group, 'blocklyEditableText');
group.style.cursor = '';
}
}
@@ -590,7 +576,7 @@ export abstract class Field implements IASTNodeLocationSvg,
return;
}
this.visible_ = visible;
const root = this.getSvgRoot();
const root = this.fieldGroup_;
if (root) {
root.style.display = visible ? 'block' : 'none';
}
@@ -630,10 +616,49 @@ export abstract class Field implements IASTNodeLocationSvg,
*
* @returns The group element.
*/
getSvgRoot(): SVGGElement {
getSvgRoot(): SVGGElement|null {
return this.fieldGroup_;
}
/**
* Gets the border rectangle element.
*
* @returns The border rectangle element.
* @throws An error if the border rectangle element is not defined.
*/
protected getBorderRect(): SVGRectElement {
if (!this.borderRect_) {
throw new Error(`The border rectangle is ${this.borderRect_}.`);
}
return this.borderRect_;
}
/**
* Gets the text element.
*
* @returns The text element.
* @throws An error if the text element is not defined.
*/
protected getTextElement(): SVGTextElement {
if (!this.textElement_) {
throw new Error(`The text element is ${this.textElement_}.`);
}
return this.textElement_;
}
/**
* Gets the text content.
*
* @returns The text content.
* @throws An error if the text content is not defined.
*/
protected getTextContent(): Text {
if (!this.textContent_) {
throw new Error(`The text content is ${this.textContent_}.`);
}
return this.textContent_;
}
/**
* Updates the field to match the colour/style of the block. Should only be
* called by BlockSvg.applyColour().
@@ -726,20 +751,19 @@ export abstract class Field implements IASTNodeLocationSvg,
const constants = this.getConstants();
const halfHeight = this.size_.height / 2;
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.textElement_.setAttribute(
'x',
(this.sourceBlock_.RTL ? this.size_.width - contentWidth - xOffset :
xOffset) as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
`${
this.getSourceBlock()?.RTL ?
this.size_.width - contentWidth - xOffset :
xOffset}`);
this.textElement_.setAttribute(
'y',
(constants!.FIELD_TEXT_BASELINE_CENTER ?
halfHeight :
halfHeight - constants!.FIELD_TEXT_HEIGHT / 2 +
constants!.FIELD_TEXT_BASELINE) as AnyDuringMigration);
`${
constants!.FIELD_TEXT_BASELINE_CENTER ?
halfHeight :
halfHeight - constants!.FIELD_TEXT_HEIGHT / 2 +
constants!.FIELD_TEXT_BASELINE}`);
}
/** Position a field's border rect after a size change. */
@@ -747,24 +771,12 @@ export abstract class Field implements IASTNodeLocationSvg,
if (!this.borderRect_) {
return;
}
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.borderRect_.setAttribute('width', `${this.size_.width}`);
this.borderRect_.setAttribute('height', `${this.size_.height}`);
this.borderRect_.setAttribute(
'width', this.size_.width as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
'rx', `${this.getConstants()!.FIELD_BORDER_RECT_RADIUS}`);
this.borderRect_.setAttribute(
'height', this.size_.height as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.borderRect_.setAttribute(
'rx',
this.getConstants()!.FIELD_BORDER_RECT_RADIUS as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.borderRect_.setAttribute(
'ry',
this.getConstants()!.FIELD_BORDER_RECT_RADIUS as AnyDuringMigration);
'ry', `${this.getConstants()!.FIELD_BORDER_RECT_RADIUS}`);
}
/**
@@ -785,10 +797,13 @@ export abstract class Field implements IASTNodeLocationSvg,
} else if (this.visible_ && this.size_.width === 0) {
// If the field is not visible the width will be 0 as well, one of the
// problems with the old system.
console.warn(
'Deprecated use of setting size_.width to 0 to rerender a' +
' field. Set field.isDirty_ to true instead.');
this.render_();
// Don't issue a warning if the field is actually zero width.
if (this.size_.width !== 0) {
console.warn(
'Deprecated use of setting size_.width to 0 to rerender a' +
' field. Set field.isDirty_ to true instead.');
}
}
return this.size_;
}
@@ -805,12 +820,17 @@ export abstract class Field implements IASTNodeLocationSvg,
let scaledWidth;
let scaledHeight;
let xy;
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
if (!this.borderRect_) {
// Browsers are inconsistent in what they return for a bounding box.
// - Webkit / Blink: fill-box / object bounding box
// - Gecko: stroke-box
const bBox = (this.sourceBlock_ as BlockSvg).getHeightWidth();
const scale = (this.sourceBlock_.workspace as WorkspaceSvg).scale;
const scale = (block.workspace as WorkspaceSvg).scale;
xy = this.getAbsoluteXY_();
scaledWidth = (bBox.width + 1) * scale;
scaledHeight = (bBox.height + 1) * scale;
@@ -896,9 +916,7 @@ export abstract class Field implements IASTNodeLocationSvg,
*/
markDirty() {
this.isDirty_ = true;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'ConstantProvider'.
this.constants_ = null as AnyDuringMigration;
this.constants_ = null;
}
/**
@@ -1050,7 +1068,7 @@ export abstract class Field implements IASTNodeLocationSvg,
* @param e Mouse down event.
*/
protected onMouseDown_(e: Event) {
if (!this.sourceBlock_ || this.sourceBlock_.disposed) {
if (!this.sourceBlock_ || this.sourceBlock_.isDeadOrDying()) {
return;
}
const gesture = (this.sourceBlock_.workspace as WorkspaceSvg).getGesture(e);
@@ -1101,7 +1119,7 @@ export abstract class Field implements IASTNodeLocationSvg,
*
* @returns Element to bind click handler to.
*/
protected getClickTarget_(): Element {
protected getClickTarget_(): Element|null {
return this.clickTarget_ || this.getSvgRoot();
}
@@ -1145,7 +1163,10 @@ export abstract class Field implements IASTNodeLocationSvg,
*/
getParentInput(): Input {
let parentInput = null;
const block = this.sourceBlock_;
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const inputs = block.inputList;
for (let idx = 0; idx < block.inputList.length; idx++) {
@@ -1158,9 +1179,7 @@ export abstract class Field implements IASTNodeLocationSvg,
}
}
}
// AnyDuringMigration because: Type 'Input | null' is not assignable to
// type 'Input'.
return parentInput as AnyDuringMigration;
return parentInput!;
}
/**
@@ -1199,12 +1218,13 @@ export abstract class Field implements IASTNodeLocationSvg,
*/
setCursorSvg(cursorSvg: SVGElement) {
if (!cursorSvg) {
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGElement'.
this.cursorSvg_ = null as AnyDuringMigration;
this.cursorSvg_ = null;
return;
}
if (!this.fieldGroup_) {
throw new Error(`The field group is ${this.fieldGroup_}.`);
}
this.fieldGroup_.appendChild(cursorSvg);
this.cursorSvg_ = cursorSvg;
}
@@ -1217,19 +1237,24 @@ export abstract class Field implements IASTNodeLocationSvg,
*/
setMarkerSvg(markerSvg: SVGElement) {
if (!markerSvg) {
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGElement'.
this.markerSvg_ = null as AnyDuringMigration;
this.markerSvg_ = null;
return;
}
if (!this.fieldGroup_) {
throw new Error(`The field group is ${this.fieldGroup_}.`);
}
this.fieldGroup_.appendChild(markerSvg);
this.markerSvg_ = markerSvg;
}
/** Redraw any attached marker or cursor svgs if needed. */
protected updateMarkers_() {
const workspace = this.sourceBlock_.workspace as WorkspaceSvg;
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const workspace = block.workspace as WorkspaceSvg;
if (workspace.keyboardAccessibilityMode && this.cursorSvg_) {
workspace.getCursor()!.draw();
}
@@ -1246,3 +1271,23 @@ export abstract class Field implements IASTNodeLocationSvg,
export interface FieldConfig {
tooltip?: string;
}
/**
* For use by Field and descendants of Field. Constructors can change
* in descendants, though they should contain all of Field's prototype methods.
*/
export type FieldProto = Pick<typeof Field, 'prototype'>;
/**
* Represents an error where the field is trying to access its block or
* information about its block before it has actually been attached to said
* block.
*/
export class UnattachedFieldError extends Error {
/** @internal */
constructor() {
super(
'The field has not yet been attached to its input. ' +
'Call appendField to attach it.');
}
}

View File

@@ -16,7 +16,7 @@ import {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as Css from './css.js';
import * as dropDownDiv from './dropdowndiv.js';
import {Field} from './field.js';
import {Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {FieldTextInputConfig, FieldTextInput} from './field_textinput.js';
import * as dom from './utils/dom.js';
@@ -70,19 +70,36 @@ export class FieldAngle extends FieldTextInput {
* otherwise SVG crops off half the border at the edges.
*/
static readonly RADIUS: number = FieldAngle.HALF - 1;
private clockwise_: boolean;
private offset_: number;
private wrap_: number;
private round_: number;
/**
* Whether the angle should increase as the angle picker is moved clockwise
* (true) or counterclockwise (false).
*/
private clockwise_ = FieldAngle.CLOCKWISE;
/**
* The offset of zero degrees (and all other angles).
*/
private offset_ = FieldAngle.OFFSET;
/**
* The maximum angle to allow before wrapping.
*/
private wrap_ = FieldAngle.WRAP;
/**
* The amount to round angles to when using a mouse or keyboard nav input.
*/
private round_ = FieldAngle.ROUND;
/** The angle picker's SVG element. */
private editor_: SVGElement|null = null;
private editor_: SVGSVGElement|null = null;
/** The angle picker's gauge path depending on the value. */
gauge_: SVGElement|null = null;
gauge_: SVGPathElement|null = null;
/** The angle picker's line drawn representing the value's angle. */
line_: SVGElement|null = null;
line_: SVGLineElement|null = null;
/** The degree symbol for this field. */
// AnyDuringMigration because: Type 'null' is not assignable to type
@@ -122,35 +139,6 @@ export class FieldAngle extends FieldTextInput {
opt_config?: FieldAngleConfig) {
super(Field.SKIP_SETUP);
/**
* Should the angle increase as the angle picker is moved clockwise (true)
* or counterclockwise (false)
*
* @see FieldAngle.CLOCKWISE
*/
this.clockwise_ = FieldAngle.CLOCKWISE;
/**
* The offset of zero degrees (and all other angles).
*
* @see FieldAngle.OFFSET
*/
this.offset_ = FieldAngle.OFFSET;
/**
* The maximum angle to allow before wrapping.
*
* @see FieldAngle.WRAP
*/
this.wrap_ = FieldAngle.WRAP;
/**
* The amount to round angles to when using a mouse or keyboard nav input.
*
* @see FieldAngle.ROUND
*/
this.round_ = FieldAngle.ROUND;
if (opt_value === Field.SKIP_SETUP) {
return;
}
@@ -202,7 +190,7 @@ export class FieldAngle extends FieldTextInput {
// #2380)
this.symbol_ = dom.createSvgElement(Svg.TSPAN, {});
this.symbol_.appendChild(document.createTextNode('°'));
this.textElement_.appendChild(this.symbol_);
this.getTextElement().appendChild(this.symbol_);
}
/** Updates the graph when the field rerenders. */
@@ -223,9 +211,7 @@ export class FieldAngle extends FieldTextInput {
super.showEditor_(opt_e, noFocus);
this.dropdownCreate_();
// AnyDuringMigration because: Argument of type 'SVGElement | null' is not
// assignable to parameter of type 'Node'.
dropDownDiv.getContentDiv().appendChild(this.editor_ as AnyDuringMigration);
dropDownDiv.getContentDiv().appendChild(this.editor_!);
if (this.sourceBlock_ instanceof BlockSvg) {
dropDownDiv.setColour(
@@ -426,26 +412,23 @@ export class FieldAngle extends FieldTextInput {
*/
protected override onHtmlInputKeyDown_(e: Event) {
super.onHtmlInputKeyDown_(e);
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const keyboardEvent = e as KeyboardEvent;
let multiplier;
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
if ((e as AnyDuringMigration).keyCode === KeyCodes.LEFT) {
if (keyboardEvent.keyCode === KeyCodes.LEFT) {
// decrement (increment in RTL)
multiplier = this.sourceBlock_.RTL ? 1 : -1;
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.RIGHT) {
multiplier = block.RTL ? 1 : -1;
} else if (keyboardEvent.keyCode === KeyCodes.RIGHT) {
// increment (decrement in RTL)
multiplier = this.sourceBlock_.RTL ? -1 : 1;
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.DOWN) {
multiplier = block.RTL ? -1 : 1;
} else if (keyboardEvent.keyCode === KeyCodes.DOWN) {
// decrement
multiplier = -1;
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.UP) {
} else if (keyboardEvent.keyCode === KeyCodes.UP) {
// increment
multiplier = 1;
}

View File

@@ -15,6 +15,7 @@ goog.declareModuleId('Blockly.FieldCheckbox');
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';
import * as dom from './utils/dom.js';
import {FieldConfig, Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import type {Sentinel} from './utils/sentinel.js';
@@ -111,8 +112,9 @@ export class FieldCheckbox extends Field {
override initView() {
super.initView();
this.textElement_.classList.add('blocklyCheckbox');
this.textElement_.style.display = this.value_ ? 'block' : 'none';
const textElement = this.getTextElement();
dom.addClass(textElement, 'blocklyCheckbox');
textElement.style.display = this.value_ ? 'block' : 'none';
}
override render_() {

View File

@@ -18,6 +18,7 @@ import './events/events_block_change.js';
import {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as Css from './css.js';
import * as dom from './utils/dom.js';
import * as dropDownDiv from './dropdowndiv.js';
import {FieldConfig, Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
@@ -190,7 +191,7 @@ export class FieldColour extends Field {
this.getConstants()!.FIELD_COLOUR_DEFAULT_HEIGHT);
if (!this.getConstants()!.FIELD_COLOUR_FULL_BLOCK) {
this.createBorderRect_();
this.borderRect_.style['fillOpacity'] = '1';
this.getBorderRect().style['fillOpacity'] = '1';
} else if (this.sourceBlock_ instanceof BlockSvg) {
this.clickTarget_ = this.sourceBlock_.getSvgRoot();
}
@@ -438,7 +439,7 @@ export class FieldColour extends Field {
(this.picker_ as AnyDuringMigration)!.blur();
const highlighted = this.getHighlighted_();
if (highlighted) {
highlighted.classList.remove('blocklyColourHighlighted');
dom.removeClass(highlighted, 'blocklyColourHighlighted');
}
}
@@ -473,10 +474,10 @@ export class FieldColour extends Field {
// Unhighlight the current item.
const highlighted = this.getHighlighted_();
if (highlighted) {
highlighted.classList.remove('blocklyColourHighlighted');
dom.removeClass(highlighted, 'blocklyColourHighlighted');
}
// Highlight new item.
cell.classList.add('blocklyColourHighlighted');
dom.addClass(cell, 'blocklyColourHighlighted');
// Set new highlighted index.
this.highlightedIndex_ = index;

View File

@@ -16,7 +16,7 @@ goog.declareModuleId('Blockly.FieldDropdown');
import type {BlockSvg} from './block_svg.js';
import * as dropDownDiv from './dropdowndiv.js';
import {FieldConfig, Field} from './field.js';
import {FieldConfig, Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {Menu} from './menu.js';
import {MenuItem} from './menuitem.js';
@@ -27,8 +27,6 @@ import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
import * as utilsString from './utils/string.js';
import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
/**
* Class for an editable dropdown field.
@@ -44,7 +42,8 @@ export class FieldDropdown extends Field {
* height.
*/
static MAX_MENU_HEIGHT_VH = 0.45;
static ARROW_CHAR: AnyDuringMigration;
static ARROW_CHAR = '▾';
/** A reference to the currently selected menu item. */
private selectedMenuItem_: MenuItem|null = null;
@@ -71,14 +70,11 @@ export class FieldDropdown extends Field {
/** Mouse cursor style when over the hotspot that initiates the editor. */
override CURSOR = 'default';
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
protected menuGenerator_!: AnyDuringMigration[][]|
((this: FieldDropdown) => AnyDuringMigration[][]);
protected menuGenerator_?: MenuGenerator;
/** A cache of the most recently generated options. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'string[][]'.
private generatedOptions_: string[][] = null as AnyDuringMigration;
private generatedOptions_: MenuOption[]|null = null;
/**
* The prefix field label, of common words set after options are trimmed.
@@ -95,7 +91,7 @@ export class FieldDropdown extends Field {
override suffixField: string|null = null;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
private selectedOption_!: Array<string|ImageProperties>;
override clickTarget_: AnyDuringMigration;
override clickTarget_: SVGElement|null = null;
/**
* @param menuGenerator A non-empty array of options for a dropdown list, or a
@@ -114,30 +110,31 @@ export class FieldDropdown extends Field {
* @throws {TypeError} If `menuGenerator` options are incorrectly structured.
*/
constructor(
menuGenerator: AnyDuringMigration[][]|Function|Sentinel,
opt_validator?: Function, opt_config?: FieldConfig) {
menuGenerator: MenuGenerator,
opt_validator?: Function,
opt_config?: FieldConfig,
);
constructor(menuGenerator: Sentinel);
constructor(
menuGenerator: MenuGenerator|Sentinel,
opt_validator?: Function,
opt_config?: FieldConfig,
) {
super(Field.SKIP_SETUP);
// If we pass SKIP_SETUP, don't do *anything* with the menu generator.
if (menuGenerator === Field.SKIP_SETUP) {
return;
}
if (!isMenuGenerator(menuGenerator)) return;
if (Array.isArray(menuGenerator)) {
validateOptions(menuGenerator);
// Deep copy the option structure so it doesn't change.
menuGenerator = JSON.parse(JSON.stringify(menuGenerator));
const trimmed = trimOptions(menuGenerator);
this.menuGenerator_ = trimmed.options;
this.prefixField = trimmed.prefix || null;
this.suffixField = trimmed.suffix || null;
} else {
this.menuGenerator_ = menuGenerator;
}
/**
* An array of options for a dropdown list,
* or a function which generates these options.
*/
this.menuGenerator_ = menuGenerator as AnyDuringMigration[][] |
((this: FieldDropdown) => AnyDuringMigration[][]);
this.trimOptions_();
/**
* The currently selected option. The field is initialized with the
* first option selected.
@@ -205,7 +202,7 @@ export class FieldDropdown extends Field {
}
if (this.borderRect_) {
this.borderRect_.classList.add('blocklyDropdownRect');
dom.addClass(this.borderRect_, 'blocklyDropdownRect');
}
}
@@ -217,24 +214,19 @@ export class FieldDropdown extends Field {
protected shouldAddBorderRect_(): boolean {
return !this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW &&
!this.sourceBlock_.isShadow();
!this.getSourceBlock()?.isShadow();
}
/** Create a tspan based arrow. */
protected createTextArrow_() {
this.arrow_ = dom.createSvgElement(Svg.TSPAN, {}, this.textElement_);
this.arrow_!.appendChild(document.createTextNode(
this.sourceBlock_.RTL ? FieldDropdown.ARROW_CHAR + ' ' :
' ' + FieldDropdown.ARROW_CHAR));
if (this.sourceBlock_.RTL) {
// AnyDuringMigration because: Argument of type 'SVGTSpanElement | null'
// is not assignable to parameter of type 'Node'.
this.textElement_.insertBefore(
this.arrow_ as AnyDuringMigration, this.textContent_);
this.getSourceBlock()?.RTL ? FieldDropdown.ARROW_CHAR + ' ' :
' ' + FieldDropdown.ARROW_CHAR));
if (this.getSourceBlock()?.RTL) {
this.getTextElement().insertBefore(this.arrow_, this.textContent_);
} else {
// AnyDuringMigration because: Argument of type 'SVGTSpanElement | null'
// is not assignable to parameter of type 'Node'.
this.textElement_.appendChild(this.arrow_ as AnyDuringMigration);
this.getTextElement().appendChild(this.arrow_);
}
}
@@ -257,17 +249,14 @@ export class FieldDropdown extends Field {
* @param opt_e Optional mouse event that triggered the field to open, or
* undefined if triggered programmatically.
*/
protected override showEditor_(opt_e?: Event) {
protected override showEditor_(opt_e?: MouseEvent) {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
this.dropdownCreate_();
// AnyDuringMigration because: Property 'clientX' does not exist on type
// 'Event'.
if (opt_e && typeof (opt_e as AnyDuringMigration).clientX === 'number') {
// AnyDuringMigration because: Property 'clientY' does not exist on type
// 'Event'. AnyDuringMigration because: Property 'clientX' does not exist
// on type 'Event'.
this.menu_!.openingCoords = new Coordinate(
(opt_e as AnyDuringMigration).clientX,
(opt_e as AnyDuringMigration).clientY);
if (opt_e && typeof opt_e.clientX === 'number') {
this.menu_!.openingCoords = new Coordinate(opt_e.clientX, opt_e.clientY);
} else {
this.menu_!.openingCoords = null;
}
@@ -276,14 +265,13 @@ export class FieldDropdown extends Field {
dropDownDiv.clearContent();
// Element gets created in render.
const menuElement = this.menu_!.render(dropDownDiv.getContentDiv());
menuElement.classList.add('blocklyDropdownMenu');
dom.addClass(menuElement, 'blocklyDropdownMenu');
if (this.getConstants()!.FIELD_DROPDOWN_COLOURED_DIV) {
const primaryColour = this.sourceBlock_.isShadow() ?
this.sourceBlock_.getParent()!.getColour() :
this.sourceBlock_.getColour();
const borderColour = this.sourceBlock_.isShadow() ?
(this.sourceBlock_.getParent() as BlockSvg).style.colourTertiary :
const primaryColour =
block.isShadow() ? block.getParent()!.getColour() : block.getColour();
const borderColour = block.isShadow() ?
(block.getParent() as BlockSvg).style.colourTertiary :
(this.sourceBlock_ as BlockSvg).style.colourTertiary;
dropDownDiv.setColour(primaryColour, borderColour);
}
@@ -304,6 +292,10 @@ export class FieldDropdown extends Field {
/** Create the dropdown editor. */
private dropdownCreate_() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const menu = new Menu();
menu.setRole(aria.Role.LISTBOX);
this.menu_ = menu;
@@ -311,18 +303,20 @@ export class FieldDropdown extends Field {
const options = this.getOptions(false);
this.selectedMenuItem_ = null;
for (let i = 0; i < options.length; i++) {
let content = options[i][0]; // Human-readable text or image.
const value = options[i][1]; // Language-neutral value.
if (typeof content === 'object') {
// An image, not text.
const image = new Image(content['width'], content['height']);
image.src = content['src'];
image.alt = content['alt'] || '';
content = image;
}
const [label, value] = options[i];
const content = (() => {
if (typeof label === 'object') {
// Convert ImageProperties to an HTMLImageElement.
const image = new Image(label['width'], label['height']);
image.src = label['src'];
image.alt = label['alt'] || '';
return image;
}
return label;
})();
const menuItem = new MenuItem(content, value);
menuItem.setRole(aria.Role.OPTION);
menuItem.setRightToLeft(this.sourceBlock_.RTL);
menuItem.setRightToLeft(block.RTL);
menuItem.setCheckable(true);
menu.addChild(menuItem);
menuItem.setChecked(value === this.value_);
@@ -365,57 +359,6 @@ export class FieldDropdown extends Field {
this.setValue(menuItem.getValue());
}
/**
* Factor out common words in statically defined options.
* Create prefix and/or suffix labels.
*/
private trimOptions_() {
const options = this.menuGenerator_;
if (!Array.isArray(options)) {
return;
}
let hasImages = false;
// Localize label text and image alt text.
for (let i = 0; i < options.length; i++) {
const label = options[i][0];
if (typeof label === 'string') {
options[i][0] = parsing.replaceMessageReferences(label);
} else {
if (label.alt !== null) {
options[i][0].alt = parsing.replaceMessageReferences(label.alt);
}
hasImages = true;
}
}
if (hasImages || options.length < 2) {
return; // Do nothing if too few items or at least one label is an image.
}
const strings = [];
for (let i = 0; i < options.length; i++) {
strings.push(options[i][0]);
}
const shortest = utilsString.shortestStringLength(strings);
const prefixLength = utilsString.commonWordPrefix(strings, shortest);
const suffixLength = utilsString.commonWordSuffix(strings, shortest);
if (!prefixLength && !suffixLength) {
return;
}
if (shortest <= prefixLength + suffixLength) {
// One or more strings will entirely vanish if we proceed. Abort.
return;
}
if (prefixLength) {
this.prefixField = strings[0].substring(0, prefixLength - 1);
}
if (suffixLength) {
this.suffixField = strings[0].substr(1 - suffixLength);
}
this.menuGenerator_ =
FieldDropdown.applyTrim_(options, prefixLength, suffixLength);
}
/**
* @returns True if the option list is generated by a function.
* Otherwise false.
@@ -433,18 +376,18 @@ export class FieldDropdown extends Field {
* (human-readable text or image, language-neutral name).
* @throws {TypeError} If generated options are incorrectly structured.
*/
getOptions(opt_useCache?: boolean): AnyDuringMigration[][] {
if (this.isOptionListDynamic()) {
if (!this.generatedOptions_ || !opt_useCache) {
// AnyDuringMigration because: Property 'call' does not exist on type
// 'any[][] | ((this: FieldDropdown) => any[][])'.
this.generatedOptions_ =
(this.menuGenerator_ as AnyDuringMigration).call(this);
validateOptions(this.generatedOptions_);
}
return this.generatedOptions_;
getOptions(opt_useCache?: boolean): MenuOption[] {
if (!this.menuGenerator_) {
// A subclass improperly skipped setup without defining the menu
// generator.
throw TypeError('A menu generator was never defined.');
}
return this.menuGenerator_ as string[][];
if (Array.isArray(this.menuGenerator_)) return this.menuGenerator_;
if (opt_useCache && this.generatedOptions_) return this.generatedOptions_;
this.generatedOptions_ = this.menuGenerator_();
validateOptions(this.generatedOptions_);
return this.generatedOptions_;
}
/**
@@ -453,17 +396,11 @@ export class FieldDropdown extends Field {
* @param opt_newValue The input value.
* @returns A valid language-neutral option, or null if invalid.
*/
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
string|null {
let isValueValid = false;
protected override doClassValidation_(opt_newValue?: MenuOption[1]): string
|null {
const options = this.getOptions(true);
for (let i = 0, option; option = options[i]; i++) {
// Options are tuples of human-readable text and language-neutral values.
if (option[1] === opt_newValue) {
isValueValid = true;
break;
}
}
const isValueValid = options.some((option) => option[1] === opt_newValue);
if (!isValueValid) {
if (this.sourceBlock_) {
console.warn(
@@ -482,7 +419,7 @@ export class FieldDropdown extends Field {
* @param newValue The value to be saved. The default validator guarantees
* that this is one of the valid dropdown options.
*/
protected override doValueUpdate_(newValue: AnyDuringMigration) {
protected override doValueUpdate_(newValue: MenuOption[1]) {
super.doValueUpdate_(newValue);
const options = this.getOptions(true);
for (let i = 0, option; option = options[i]; i++) {
@@ -520,7 +457,7 @@ export class FieldDropdown extends Field {
/** Draws the border with the correct width. */
protected override render_() {
// Hide both elements.
this.textContent_.nodeValue = '';
this.getTextContent().nodeValue = '';
this.imageElement_!.style.display = 'none';
// Show correct element.
@@ -540,17 +477,15 @@ export class FieldDropdown extends Field {
* @param imageJson Selected option that must be an image.
*/
private renderSelectedImage_(imageJson: ImageProperties) {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
this.imageElement_!.style.display = '';
this.imageElement_!.setAttributeNS(
dom.XLINK_NS, 'xlink:href', imageJson.src);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.imageElement_!.setAttribute(
'height', imageJson.height as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.imageElement_!.setAttribute(
'width', imageJson.width as AnyDuringMigration);
this.imageElement_!.setAttribute('height', `${imageJson.height}`);
this.imageElement_!.setAttribute('width', `${imageJson.width}`);
const imageHeight = Number(imageJson.height);
const imageWidth = Number(imageJson.width);
@@ -578,12 +513,12 @@ export class FieldDropdown extends Field {
this.size_.height = height;
let arrowX = 0;
if (this.sourceBlock_.RTL) {
if (block.RTL) {
const imageX = xPadding + arrowWidth;
this.imageElement_!.setAttribute('x', imageX.toString());
} else {
arrowX = imageWidth + arrowWidth;
this.textElement_.setAttribute('text-anchor', 'end');
this.getTextElement().setAttribute('text-anchor', 'end');
this.imageElement_!.setAttribute('x', xPadding.toString());
}
this.imageElement_!.setAttribute(
@@ -595,9 +530,10 @@ export class FieldDropdown extends Field {
/** Renders the selected option, which must be text. */
private renderSelectedText_() {
// Retrieves the selected option to display through getText_.
this.textContent_.nodeValue = this.getDisplayText_();
this.textElement_.classList.add('blocklyDropdownText');
this.textElement_.setAttribute('text-anchor', 'start');
this.getTextContent().nodeValue = this.getDisplayText_();
const textElement = this.getTextElement();
dom.addClass(textElement, 'blocklyDropdownText');
textElement.setAttribute('text-anchor', 'start');
// Height and width include the border rect.
const hasBorder = !!this.borderRect_;
@@ -605,7 +541,7 @@ export class FieldDropdown extends Field {
hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
this.getConstants()!.FIELD_TEXT_HEIGHT);
const textWidth = dom.getFastTextWidth(
this.textElement_, this.getConstants()!.FIELD_TEXT_FONTSIZE,
this.getTextElement(), this.getConstants()!.FIELD_TEXT_FONTSIZE,
this.getConstants()!.FIELD_TEXT_FONTWEIGHT,
this.getConstants()!.FIELD_TEXT_FONTFAMILY);
const xPadding =
@@ -633,12 +569,16 @@ export class FieldDropdown extends Field {
if (!this.svgArrow_) {
return 0;
}
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const hasBorder = !!this.borderRect_;
const xPadding =
hasBorder ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : 0;
const textPadding = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_PADDING;
const svgArrowSize = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE;
const arrowX = this.sourceBlock_.RTL ? xPadding : x + textPadding;
const arrowX = block.RTL ? xPadding : x + textPadding;
this.svgArrow_.setAttribute(
'transform', 'translate(' + arrowX + ',' + y + ')');
return svgArrowSize + textPadding;
@@ -681,30 +621,6 @@ export class FieldDropdown extends Field {
// override the static fromJson method.
return new this(options.options, undefined, options);
}
/**
* Use the calculated prefix and suffix lengths to trim all of the options in
* the given array.
*
* @param options Array of option tuples:
* (human-readable text or image, language-neutral name).
* @param prefixLength The length of the common prefix.
* @param suffixLength The length of the common suffix
* @returns A new array with all of the option text trimmed.
*/
static applyTrim_(
options: AnyDuringMigration[][], prefixLength: number,
suffixLength: number): AnyDuringMigration[][] {
const newOptions = [];
// Remove the prefix and suffix from the options.
for (let i = 0; i < options.length; i++) {
let text = options[i][0];
const value = options[i][1];
text = text.substring(prefixLength, text.length - suffixLength);
newOptions[i] = [text, value];
}
return newOptions;
}
}
/**
@@ -724,6 +640,18 @@ export interface ImageProperties {
*/
export type MenuOption = [string | ImageProperties, string];
/**
* A function that generates an array of menu options for FieldDropdown
* or its descendants.
*/
export type MenuGeneratorFunction = (this: FieldDropdown) => MenuOption[];
/**
* Either an array of menu options or a function that generates an array of
* menu options for FieldDropdown or its descendants.
*/
export type MenuGenerator = MenuOption[]|MenuGeneratorFunction;
/**
* fromJson config for the dropdown field.
*/
@@ -740,8 +668,81 @@ const IMAGE_Y_OFFSET = 5;
/** The total vertical padding above and below an image. */
const IMAGE_Y_PADDING: number = IMAGE_Y_OFFSET * 2;
/** Android can't (in 2014) display "▾", so use "▼" instead. */
FieldDropdown.ARROW_CHAR = userAgent.ANDROID ? '▼' : '▾';
/**
* NOTE: Because Sentinel is an empty class, proving a value is Sentinel does
* not resolve in TS that it isn't a MenuGenerator.
*/
function isMenuGenerator(menuGenerator: MenuGenerator|
Sentinel): menuGenerator is MenuGenerator {
return menuGenerator !== Field.SKIP_SETUP;
}
/**
* Factor out common words in statically defined options.
* Create prefix and/or suffix labels.
*/
function trimOptions(options: MenuOption[]):
{options: MenuOption[]; prefix?: string; suffix?: string;} {
let hasImages = false;
const trimmedOptions = options.map(([label, value]): MenuOption => {
if (typeof label === 'string') {
return [parsing.replaceMessageReferences(label), value];
}
hasImages = true;
// Copy the image properties so they're not influenced by the original.
// NOTE: No need to deep copy since image properties are only 1 level deep.
const imageLabel = label.alt !== null ?
{...label, alt: parsing.replaceMessageReferences(label.alt)} :
{...label};
return [imageLabel, value];
});
if (hasImages || options.length < 2) return {options: trimmedOptions};
const stringOptions = trimmedOptions as [string, string][];
const stringLabels = stringOptions.map(([label]) => label);
const shortest = utilsString.shortestStringLength(stringLabels);
const prefixLength = utilsString.commonWordPrefix(stringLabels, shortest);
const suffixLength = utilsString.commonWordSuffix(stringLabels, shortest);
if ((!prefixLength && !suffixLength) ||
(shortest <= prefixLength + suffixLength)) {
// One or more strings will entirely vanish if we proceed. Abort.
return {options: stringOptions};
}
const prefix =
prefixLength ? stringLabels[0].substring(0, prefixLength - 1) : undefined;
const suffix =
suffixLength ? stringLabels[0].substr(1 - suffixLength) : undefined;
return {
options: applyTrim(stringOptions, prefixLength, suffixLength),
prefix,
suffix,
};
}
/**
* Use the calculated prefix and suffix lengths to trim all of the options in
* the given array.
*
* @param options Array of option tuples:
* (human-readable text or image, language-neutral name).
* @param prefixLength The length of the common prefix.
* @param suffixLength The length of the common suffix
* @returns A new array with all of the option text trimmed.
*/
function applyTrim(
options: [string, string][], prefixLength: number,
suffixLength: number): MenuOption[] {
return options.map(
([text, value]) =>
[text.substring(prefixLength, text.length - suffixLength),
value,
]);
}
/**
* Validates the data structure to be processed as an options list.
@@ -749,7 +750,7 @@ FieldDropdown.ARROW_CHAR = userAgent.ANDROID ? '▼' : '▾';
* @param options The proposed dropdown options.
* @throws {TypeError} If proposed options are incorrectly structured.
*/
function validateOptions(options: AnyDuringMigration) {
function validateOptions(options: MenuOption[]) {
if (!Array.isArray(options)) {
throw TypeError('FieldDropdown options must be an array.');
}

View File

@@ -13,6 +13,7 @@
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldLabel');
import * as dom from './utils/dom.js';
import {FieldConfig, Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import * as parsing from './utils/parsing.js';
@@ -75,7 +76,7 @@ export class FieldLabel extends Field {
override initView() {
this.createTextElement_();
if (this.class_) {
this.textElement_.classList.add(this.class_);
dom.addClass(this.getTextElement(), this.class_);
}
}
@@ -101,10 +102,10 @@ export class FieldLabel extends Field {
setClass(cssClass: string|null) {
if (this.textElement_) {
if (this.class_) {
this.textElement_.classList.remove(this.class_);
dom.removeClass(this.textElement_, this.class_);
}
if (cssClass) {
this.textElement_.classList.add(cssClass);
dom.addClass(this.textElement_, cssClass);
}
}
this.class_ = cssClass;

View File

@@ -13,7 +13,7 @@ import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldMultilineInput');
import * as Css from './css.js';
import {Field} from './field.js';
import {Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {FieldTextInputConfig, FieldTextInput} from './field_textinput.js';
import * as aria from './utils/aria.js';
@@ -163,6 +163,10 @@ export class FieldMultilineInput extends FieldTextInput {
* @returns Currently displayed text.
*/
protected override getDisplayText_(): string {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
let textLines = this.getText();
if (!textLines) {
// Prevent the field from disappearing if empty.
@@ -189,8 +193,8 @@ export class FieldMultilineInput extends FieldTextInput {
textLines += '\n';
}
}
if (this.sourceBlock_.RTL) {
// The SVG is LTR, force value to be RTL by adding an RLM.
if (block.RTL) {
// The SVG is LTR, force value to be RTL.
textLines += '\u200F';
}
return textLines;
@@ -212,6 +216,10 @@ export class FieldMultilineInput extends FieldTextInput {
/** Updates the text of the textElement. */
protected override render_() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
// Remove all text group children.
let currentChild;
while (currentChild = this.textGroup_.firstChild) {
@@ -239,16 +247,16 @@ export class FieldMultilineInput extends FieldTextInput {
if (this.isBeingEdited_) {
const htmlInput = this.htmlInput_ as HTMLElement;
if (this.isOverflowedY_) {
htmlInput.classList.add('blocklyHtmlTextAreaInputOverflowedY');
dom.addClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
} else {
htmlInput.classList.remove('blocklyHtmlTextAreaInputOverflowedY');
dom.removeClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
}
}
this.updateSize_();
if (this.isBeingEdited_) {
if (this.sourceBlock_.RTL) {
if (block.RTL) {
// in RTL, we need to let the browser reflow before resizing
// in order to get the correct bounding box of the borderRect
// avoiding issue #2777.
@@ -258,10 +266,10 @@ export class FieldMultilineInput extends FieldTextInput {
}
const htmlInput = this.htmlInput_ as HTMLElement;
if (!this.isTextValid_) {
htmlInput.classList.add('blocklyInvalidInput');
dom.addClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, true);
} else {
htmlInput.classList.remove('blocklyInvalidInput');
dom.removeClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, false);
}
}
@@ -270,11 +278,15 @@ export class FieldMultilineInput extends FieldTextInput {
/** Updates the size of the field based on the text. */
protected override updateSize_() {
const nodes = this.textGroup_.childNodes;
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE;
const fontWeight = this.getConstants()!.FIELD_TEXT_FONTWEIGHT;
const fontFamily = this.getConstants()!.FIELD_TEXT_FONTFAMILY;
let totalWidth = 0;
let totalHeight = 0;
for (let i = 0; i < nodes.length; i++) {
const tspan = nodes[i] as SVGTextElement;
const textWidth = dom.getTextWidth(tspan);
const textWidth =
dom.getFastTextWidth(tspan, fontSize, fontWeight, fontFamily);
if (textWidth > totalWidth) {
totalWidth = textWidth;
}
@@ -290,9 +302,6 @@ export class FieldMultilineInput extends FieldTextInput {
const actualEditorLines = this.value_.split('\n');
const dummyTextElement = dom.createSvgElement(
Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'});
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE;
const fontWeight = this.getConstants()!.FIELD_TEXT_FONTWEIGHT;
const fontFamily = this.getConstants()!.FIELD_TEXT_FONTFAMILY;
for (let i = 0; i < actualEditorLines.length; i++) {
if (actualEditorLines[i].length > this.maxDisplayLength) {

View File

@@ -75,7 +75,7 @@ function fromJsonInternal(options: AnyDuringMigration): Field|null {
' the file is not loaded, the field does not register itself (Issue' +
' #1584), or the registration is not being reached.');
return null;
} else if (typeof (fieldObject as any)['fromJson'] !== 'function') {
} else if (typeof (fieldObject as any).fromJson !== 'function') {
throw new TypeError('returned Field was not a IRegistrableField');
} else {
return (fieldObject as unknown as IRegistrableField).fromJson(options);

View File

@@ -18,9 +18,10 @@ import './events/events_block_change.js';
import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as dialog from './dialog.js';
import * as dom from './utils/dom.js';
import * as dropDownDiv from './dropdowndiv.js';
import * as eventUtils from './events/utils.js';
import {FieldConfig, Field} from './field.js';
import {FieldConfig, Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {Msg} from './msg.js';
import * as aria from './utils/aria.js';
@@ -126,13 +127,17 @@ export class FieldTextInput extends Field {
/** @internal */
override initView() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
if (this.getConstants()!.FULL_BLOCK_FIELDS) {
// Step one: figure out if this is the only field on this block.
// Rendering is quite different in that case.
let nFields = 0;
let nConnections = 0;
// Count the number of fields, excluding text fields
for (let i = 0, input; input = this.sourceBlock_.inputList[i]; i++) {
for (let i = 0, input; input = block.inputList[i]; i++) {
for (let j = 0; input.fieldRow[j]; j++) {
nFields++;
}
@@ -143,7 +148,7 @@ export class FieldTextInput extends Field {
// The special case is when this is the only non-label field on the block
// and it has an output but no inputs.
this.fullBlockClickTarget_ =
nFields <= 1 && this.sourceBlock_.outputConnection && !nConnections;
nFields <= 1 && block.outputConnection && !nConnections;
} else {
this.fullBlockClickTarget_ = false;
}
@@ -216,15 +221,15 @@ export class FieldTextInput extends Field {
* @internal
*/
override applyColour() {
if (this.sourceBlock_ && this.getConstants()!.FULL_BLOCK_FIELDS) {
if (this.borderRect_) {
this.borderRect_.setAttribute(
'stroke', (this.sourceBlock_ as BlockSvg).style.colourTertiary);
} else {
(this.sourceBlock_ as BlockSvg)
.pathObject.svgPath.setAttribute(
'fill', this.getConstants()!.FIELD_BORDER_RECT_COLOUR);
}
if (!this.sourceBlock_ || !this.getConstants()!.FULL_BLOCK_FIELDS) return;
const source = this.sourceBlock_ as BlockSvg;
if (this.borderRect_) {
this.borderRect_.setAttribute('stroke', source.style.colourTertiary);
} else {
source.pathObject.svgPath.setAttribute(
'fill', this.getConstants()!.FIELD_BORDER_RECT_COLOUR);
}
}
@@ -240,10 +245,10 @@ export class FieldTextInput extends Field {
this.resizeEditor_();
const htmlInput = this.htmlInput_ as HTMLElement;
if (!this.isTextValid_) {
htmlInput.classList.add('blocklyInvalidInput');
dom.addClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, true);
} else {
htmlInput.classList.remove('blocklyInvalidInput');
dom.removeClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, false);
}
}
@@ -306,7 +311,11 @@ export class FieldTextInput extends Field {
* @param quietInput True if editor should be created without focus.
*/
private showInlineEditor_(quietInput: boolean) {
WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_.bind(this));
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
WidgetDiv.show(this, block.RTL, this.widgetDispose_.bind(this));
this.htmlInput_ = this.widgetCreate_() as HTMLInputElement;
this.isBeingEdited_ = true;
@@ -324,10 +333,16 @@ export class FieldTextInput extends Field {
* @returns The newly created text input editor.
*/
protected widgetCreate_(): HTMLElement {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
eventUtils.setGroup(true);
const div = WidgetDiv.getDiv();
this.getClickTarget_().classList.add('editing');
const clickTarget = this.getClickTarget_();
if (!clickTarget) throw new Error('A click target has not been set.');
dom.addClass(clickTarget, 'editing');
const htmlInput = (document.createElement('input'));
htmlInput.className = 'blocklyHtmlInput';
@@ -347,8 +362,8 @@ export class FieldTextInput extends Field {
// Override border radius.
borderRadius = (bBox.bottom - bBox.top) / 2 + 'px';
// Pull stroke colour from the existing shadow block
const strokeColour = this.sourceBlock_.getParent() ?
(this.sourceBlock_.getParent() as BlockSvg).style.colourTertiary :
const strokeColour = block.getParent() ?
(block.getParent() as BlockSvg).style.colourTertiary :
(this.sourceBlock_ as BlockSvg).style.colourTertiary;
htmlInput.style.border = 1 * scale + 'px solid ' + strokeColour;
div!.style.borderRadius = borderRadius;
@@ -396,7 +411,9 @@ export class FieldTextInput extends Field {
style.boxShadow = '';
this.htmlInput_ = null;
this.getClickTarget_().classList.remove('editing');
const clickTarget = this.getClickTarget_();
if (!clickTarget) throw new Error('A click target has not been set.');
dom.removeClass(clickTarget, 'editing');
}
/**
@@ -504,6 +521,10 @@ export class FieldTextInput extends Field {
/** Resize the editor to fit the text. */
protected resizeEditor_() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const div = WidgetDiv.getDiv();
const bBox = this.getScaledBBox();
div!.style.width = bBox.right - bBox.left + 'px';
@@ -511,7 +532,7 @@ export class FieldTextInput extends Field {
// In RTL mode block fields and LTR input fields the left edge moves,
// whereas the right edge is fixed. Reposition the editor.
const x = this.sourceBlock_.RTL ? bBox.right - div!.offsetWidth : bBox.left;
const x = block.RTL ? bBox.right - div!.offsetWidth : bBox.left;
const xy = new Coordinate(x, bBox.top);
div!.style.left = xy.x + 'px';

View File

@@ -16,8 +16,8 @@ goog.declareModuleId('Blockly.FieldVariable');
import './events/events_block_change.js';
import type {Block} from './block.js';
import {Field, FieldConfig} from './field.js';
import {FieldDropdown} from './field_dropdown.js';
import {Field, FieldConfig, UnattachedFieldError} from './field.js';
import {FieldDropdown, MenuGenerator, MenuOption} from './field_dropdown.js';
import * as fieldRegistry from './field_registry.js';
import * as internalConstants from './internal_constants.js';
import type {Menu} from './menu.js';
@@ -37,8 +37,7 @@ import * as Xml from './xml.js';
* @alias Blockly.FieldVariable
*/
export class FieldVariable extends FieldDropdown {
protected override menuGenerator_: AnyDuringMigration[][]|
((this: FieldDropdown) => AnyDuringMigration[][]);
protected override menuGenerator_: MenuGenerator|undefined;
defaultVariableName: string;
/** The type of the default variable for this field. */
@@ -89,9 +88,7 @@ export class FieldVariable extends FieldDropdown {
* An array of options for a dropdown list,
* or a function which generates these options.
*/
// AnyDuringMigration because: Type '(this: FieldVariable) => any[][]' is
// not assignable to type 'any[][] | ((this: FieldDropdown) => any[][])'.
this.menuGenerator_ = FieldVariable.dropdownCreate as AnyDuringMigration;
this.menuGenerator_ = FieldVariable.dropdownCreate as MenuGenerator;
/**
* The initial variable name passed to this field's constructor, or an
@@ -135,20 +132,27 @@ export class FieldVariable extends FieldDropdown {
* @internal
*/
override initModel() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
if (this.variable_) {
return; // Initialization already happened.
}
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, null, this.defaultVariableName,
this.defaultType_);
block.workspace, null, this.defaultVariableName, this.defaultType_);
// Don't call setValue because we don't want to cause a rerender.
this.doValueUpdate_(variable.getId());
}
override shouldAddBorderRect_() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
return super.shouldAddBorderRect_() &&
(!this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
this.sourceBlock_.type !== 'variables_get');
block.type !== 'variables_get');
}
/**
@@ -158,6 +162,10 @@ export class FieldVariable extends FieldDropdown {
* field's state.
*/
override fromXml(fieldElement: Element) {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const id = fieldElement.getAttribute('id');
const variableName = fieldElement.textContent;
// 'variabletype' should be lowercase, but until July 2019 it was sometimes
@@ -168,8 +176,7 @@ export class FieldVariable extends FieldDropdown {
// AnyDuringMigration because: Argument of type 'string | null' is not
// assignable to parameter of type 'string | undefined'.
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, id, variableName as AnyDuringMigration,
variableType);
block.workspace, id, variableName as AnyDuringMigration, variableType);
// This should never happen :)
if (variableType !== null && variableType !== variable.type) {
@@ -233,12 +240,16 @@ export class FieldVariable extends FieldDropdown {
* @internal
*/
override loadState(state: AnyDuringMigration) {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
if (this.loadLegacyState(FieldVariable, state)) {
return;
}
// This is necessary so that blocks in the flyout can have custom var names.
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, state['id'] || null, state['name'],
block.workspace, state['id'] || null, state['name'],
state['type'] || '');
this.setValue(variable.getId());
}
@@ -315,8 +326,12 @@ export class FieldVariable extends FieldDropdown {
if (opt_newValue === null) {
return null;
}
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const newId = opt_newValue as string;
const variable = Variables.getVariable(this.sourceBlock_.workspace, newId);
const variable = Variables.getVariable(block.workspace, newId);
if (!variable) {
console.warn(
'Variable id doesn\'t point to a real variable! ' +
@@ -342,8 +357,11 @@ export class FieldVariable extends FieldDropdown {
* @param newId The value to be saved.
*/
protected override doValueUpdate_(newId: AnyDuringMigration) {
this.variable_ =
Variables.getVariable(this.sourceBlock_.workspace, newId as string);
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
this.variable_ = Variables.getVariable(block.workspace, newId as string);
super.doValueUpdate_(newId);
}
@@ -377,7 +395,7 @@ export class FieldVariable extends FieldDropdown {
let variableTypes = this.variableTypes;
if (variableTypes === null) {
// If variableTypes is null, return all variable types.
if (this.sourceBlock_ && !this.sourceBlock_.disposed) {
if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) {
return this.sourceBlock_.workspace.getVariableTypes();
}
}
@@ -456,7 +474,7 @@ export class FieldVariable extends FieldDropdown {
protected override onItemSelected_(menu: Menu, menuItem: MenuItem) {
const id = menuItem.getValue();
// Handle special cases.
if (this.sourceBlock_ && !this.sourceBlock_.disposed) {
if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) {
if (id === internalConstants.RENAME_VARIABLE_ID) {
// Rename variable.
Variables.renameVariable(
@@ -507,15 +525,15 @@ export class FieldVariable extends FieldDropdown {
*
* @returns Array of variable names/id tuples.
*/
static dropdownCreate(this: FieldVariable): AnyDuringMigration[][] {
static dropdownCreate(this: FieldVariable): MenuOption[] {
if (!this.variable_) {
throw Error(
'Tried to call dropdownCreate on a variable field with no' +
' variable selected.');
}
const name = this.getText();
let variableModelList: AnyDuringMigration[] = [];
if (this.sourceBlock_ && !this.sourceBlock_.disposed) {
let variableModelList: VariableModel[] = [];
if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) {
const variableTypes = this.getVariableTypes_();
// Get a copy of the list, so that adding rename and new variable options
// doesn't modify the workspace's list.
@@ -528,7 +546,7 @@ export class FieldVariable extends FieldDropdown {
}
variableModelList.sort(VariableModel.compareByName);
const options = [];
const options: [string, string][] = [];
for (let i = 0; i < variableModelList.length; i++) {
// Set the UUID as the internal representation of the variable.
options[i] = [variableModelList[i].name, variableModelList[i].getId()];

View File

@@ -11,7 +11,7 @@
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Generator');
goog.declareModuleId('Blockly.CodeGenerator');
import type {Block} from './block.js';
import * as common from './common.js';
@@ -24,14 +24,14 @@ import type {Workspace} from './workspace.js';
* Class for a code generator that translates the blocks into a language.
*
* @unrestricted
* @alias Blockly.Generator
* @alias Blockly.CodeGenerator
*/
export class Generator {
export class CodeGenerator {
name_: string;
/**
* This is used as a placeholder in functions defined using
* Generator.provideFunction_. It must not be legal code that could
* CodeGenerator.provideFunction_. It must not be legal code that could
* legitimately appear in a function definition (or comment), and it must
* not confuse the regular expression parser.
*/
@@ -205,7 +205,7 @@ export class Generator {
|[string, number] {
if (this.isInitialized === false) {
console.warn(
'Generator init was not called before blockToCode was called.');
'CodeGenerator init was not called before blockToCode was called.');
}
if (!block) {
return '';
@@ -414,7 +414,7 @@ export class Generator {
* "listRandom", not "random"). There is no danger of colliding with reserved
* words, or user-defined variable or procedure names.
*
* The code gets output when Generator.finish() is called.
* The code gets output when CodeGenerator.finish() is called.
*
* @param desiredName The desired name of the function (e.g. mathIsPrime).
* @param code A list of statements or one multi-line code string. Use ' '
@@ -514,25 +514,24 @@ export class Generator {
}
}
Object.defineProperties(Generator.prototype, {
Object.defineProperties(CodeGenerator.prototype, {
/**
* A database of variable names.
*
* @name Blockly.Generator.prototype.variableDB_
* @name Blockly.CodeGenerator.prototype.variableDB_
* @deprecated 'variableDB_' was renamed to 'nameDB_' (May 2021).
* @suppress {checkTypes}
*/
variableDB_: ({
/** @returns Name database. */
get(this: Generator): Names |
get(this: CodeGenerator): Names |
undefined {
deprecation.warn(
'variableDB_', 'May 2021', 'September 2022', 'nameDB_');
deprecation.warn('variableDB_', 'version 9', 'version 10', 'nameDB_');
return this.nameDB_;
},
/** @param nameDb New name database. */
set(this: Generator, nameDb: Names|undefined) {
deprecation.warn('variableDB_', 'May 2021', 'September2022', 'nameDB_');
set(this: CodeGenerator, nameDb: Names|undefined) {
deprecation.warn('variableDB_', 'version 9', 'version 10', 'nameDB_');
this.nameDB_ = nameDb;
},
}),

View File

@@ -20,6 +20,7 @@ import * as dom from './utils/dom.js';
import {Size} from './utils/size.js';
import {Svg} from './utils/svg.js';
import * as svgMath from './utils/svg_math.js';
import * as deprecation from './utils/deprecation.js';
/**
@@ -28,7 +29,7 @@ import * as svgMath from './utils/svg_math.js';
* @alias Blockly.Icon
*/
export abstract class Icon {
protected block_: BlockSvg;
protected block_: BlockSvg|null;
/** The icon SVG group. */
iconGroup_: SVGGElement|null = null;
@@ -45,7 +46,12 @@ export abstract class Icon {
protected iconXY_: Coordinate|null = null;
/** @param block The block associated with this icon. */
constructor(block: BlockSvg) {
constructor(block: BlockSvg|null) {
if (!block) {
deprecation.warn(
'Calling the Icon constructor with a null block', 'version 9',
'version 10', 'a non-null block');
}
this.block_ = block;
}
@@ -62,12 +68,12 @@ export abstract class Icon {
*/
this.iconGroup_ =
dom.createSvgElement(Svg.G, {'class': 'blocklyIconGroup'});
if (this.block_.isInFlyout) {
this.iconGroup_.classList.add('blocklyIconGroupReadonly');
if (this.getBlock().isInFlyout) {
dom.addClass(this.iconGroup_, 'blocklyIconGroupReadonly');
}
this.drawIcon_(this.iconGroup_);
this.block_.getSvgRoot().appendChild(this.iconGroup_);
this.getBlock().getSvgRoot().appendChild(this.iconGroup_);
browserEvents.conditionalBind(
this.iconGroup_, 'mouseup', this, this.iconClick_);
this.updateEditable();
@@ -99,19 +105,19 @@ export abstract class Icon {
* @param e Mouse click event.
*/
protected iconClick_(e: MouseEvent) {
if (this.block_.workspace.isDragging()) {
if (this.getBlock().workspace.isDragging()) {
// Drag operation is concluding. Don't open the editor.
return;
}
if (!this.block_.isInFlyout && !browserEvents.isRightButton(e)) {
if (!this.getBlock().isInFlyout && !browserEvents.isRightButton(e)) {
this.setVisible(!this.isVisible());
}
}
/** Change the colour of the associated bubble to match its block. */
applyColour() {
if (this.isVisible()) {
this.bubble_!.setColour(this.block_.style.colourPrimary);
if (this.bubble_ && this.isVisible()) {
this.bubble_.setColour(this.getBlock().style.colourPrimary);
}
}
@@ -122,8 +128,8 @@ export abstract class Icon {
*/
setIconLocation(xy: Coordinate) {
this.iconXY_ = xy;
if (this.isVisible()) {
this.bubble_!.setAnchorLocation(xy);
if (this.bubble_ && this.isVisible()) {
this.bubble_.setAnchorLocation(xy);
}
}
@@ -133,7 +139,7 @@ export abstract class Icon {
*/
computeIconLocation() {
// Find coordinates for the centre of the icon and update the arrow.
const blockXY = this.block_.getRelativeToSurfaceXY();
const blockXY = this.getBlock().getRelativeToSurfaceXY();
const iconXY = svgMath.getRelativeXY(this.iconGroup_ as SVGElement);
const newXY = new Coordinate(
blockXY.x + iconXY.x + this.SIZE / 2,
@@ -178,5 +184,16 @@ export abstract class Icon {
* @param _visible True if the icon should be visible.
*/
setVisible(_visible: boolean) {}
/**
* Returns the block this icon is attached to.
*/
protected getBlock(): BlockSvg {
if (!this.block_) {
throw new Error('Block is not set for this icon.');
}
return this.block_;
}
}
// No-op on base class

View File

@@ -172,11 +172,11 @@ function createMainWorkspace(
const injectionDiv = mainWorkspace.getInjectionDiv();
const rendererClassName = mainWorkspace.getRenderer().getClassName();
if (rendererClassName) {
injectionDiv.classList.add(rendererClassName);
dom.addClass(injectionDiv, rendererClassName);
}
const themeClassName = mainWorkspace.getTheme().getClassName();
if (themeClassName) {
injectionDiv.classList.add(themeClassName);
dom.addClass(injectionDiv, themeClassName);
}
if (!wsOptions.hasCategories && wsOptions.languageTree) {

View File

@@ -28,5 +28,5 @@ export interface IASTNodeLocationWithBlock extends IASTNodeLocation {
*
* @returns The source block.
*/
getSourceBlock(): Block;
getSourceBlock(): Block|null;
}

View File

@@ -0,0 +1,45 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* The interface for the data model of a procedure parameter.
*
* @namespace Blockly.IParameterModel
*/
/**
* A data model for a procedure.
*/
export interface IParameterModel {
/**
* Sets the name of this parameter to the given name.
*/
setName(name: string): this;
/**
* Sets the types of this parameter to the given type.
*/
setTypes(types: string[]): this;
/**
* Returns the name of this parameter.
*/
getName(): string;
/**
* Return the types of this parameter.
*/
getTypes(): string[];
/**
* Returns the unique language-neutral ID for the parameter.
*
* This represents the identify of the variable model which does not change
* over time.
*/
getId(): string;
}

View File

@@ -0,0 +1,19 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {Block} from '../block.js';
/** The interface for a block which models a procedure. */
export interface IProcedureBlock {
doProcedureUpdate(): void;
}
/** A type guard which checks if the given block is a procedure block. */
export function isProcedureBlock(block: Block|
IProcedureBlock): block is IProcedureBlock {
return (block as IProcedureBlock).doProcedureUpdate !== undefined;
}

View File

@@ -0,0 +1,20 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {IProcedureModel} from './i_procedure_model.js';
export interface IProcedureMap extends Map<string, IProcedureModel> {
/**
* Adds the given ProcedureModel to the map of procedure models, so that
* blocks can find it.
*/
add(proc: IProcedureModel): this;
/** Returns all of the procedures stored in this map. */
getProcedures(): IProcedureModel[];
}

View File

@@ -0,0 +1,70 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* The interface for the data model of a procedure.
*
* @namespace Blockly.IProcedureModel
*/
import {IParameterModel} from './i_parameter_model.js';
/**
* A data model for a procedure.
*/
export interface IProcedureModel {
/** Sets the human-readable name of the procedure. */
setName(name: string): this;
/**
* Inserts a parameter into the list of parameters.
*
* To move a parameter, first delete it, and then re-insert.
*/
insertParameter(parameterModel: IParameterModel, index: number): this;
/** Removes the parameter at the given index from the parameter list. */
deleteParameter(index: number): this;
/**
* Sets the return type(s) of the procedure.
*
* Pass null to represent a procedure that does not return.
*/
setReturnTypes(types: string[]|null): this;
/**
* Sets whether this procedure is enabled/disabled. If a procedure is disabled
* all procedure caller blocks should be disabled as well.
*/
setEnabled(enabled: boolean): this;
/** Returns the unique language-neutral ID for the procedure. */
getId(): string;
/** Returns the human-readable name of the procedure. */
getName(): string;
/** Returns the parameter at the given index in the parameter list. */
getParameter(index: number): IParameterModel;
/** Returns an array of all of the parameters in the parameter list. */
getParameters(): IParameterModel[];
/**
* Returns the return type(s) of the procedure.
*
* Null represents a procedure that does not return a value.
*/
getReturnTypes(): string[]|null;
/**
* Returns whether the procedure is enabled/disabled. If a procedure is
* disabled, all procedure caller blocks should be disabled as well.
*/
getEnabled(): boolean;
}

View File

@@ -176,6 +176,10 @@ export class ASTNode {
const location = this.location_ as Field;
const input = location.getParentInput();
const block = location.getSourceBlock();
if (!block) {
throw new Error(
'The current AST location is not associated with a block');
}
const curIdx = block.inputList.indexOf((input));
let fieldIdx = input.fieldRow.indexOf(location) + 1;
for (let i = curIdx; i < block.inputList.length; i++) {
@@ -235,6 +239,10 @@ export class ASTNode {
const location = this.location_ as Field;
const parentInput = location.getParentInput();
const block = location.getSourceBlock();
if (!block) {
throw new Error(
'The current AST location is not associated with a block');
}
const curIdx = block.inputList.indexOf((parentInput));
let fieldIdx = parentInput.fieldRow.indexOf(location) - 1;
for (let i = curIdx; i >= 0; i--) {
@@ -270,12 +278,15 @@ export class ASTNode {
// TODO(#6097): Use instanceof checks to exit early for values of
// curLocation that don't make sense.
if ((curLocation as IASTNodeLocationWithBlock).getSourceBlock) {
curLocation = (curLocation as IASTNodeLocationWithBlock).getSourceBlock();
const block = (curLocation as IASTNodeLocationWithBlock).getSourceBlock();
if (block) {
curLocation = block;
}
}
// TODO(#6097): Use instanceof checks to exit early for values of
// curLocation that don't make sense.
const curLocationAsBlock = curLocation as Block;
if (!curLocationAsBlock || curLocationAsBlock.disposed) {
if (!curLocationAsBlock || curLocationAsBlock.isDeadOrDying()) {
return null;
}
const curRoot = curLocationAsBlock.getRootBlock();
@@ -531,7 +542,12 @@ export class ASTNode {
}
case ASTNode.types.FIELD: {
const field = this.location_ as Field;
return ASTNode.createBlockNode(field.getSourceBlock());
const block = field.getSourceBlock();
if (!block) {
throw new Error(
'The current AST location is not associated with a block');
}
return ASTNode.createBlockNode(block);
}
case ASTNode.types.INPUT: {
const connection = this.location_ as Connection;

View File

@@ -86,13 +86,13 @@ Object.defineProperties(Blockly, {
mainWorkspace: {
set: function(x) {
deprecation.warn(
'Blockly.mainWorkspace', 'August 2022', 'September 2022',
'Blockly.mainWorkspace', 'version 9', 'version 10',
'Blockly.getMainWorkspace');
common.setMainWorkspace(x);
},
get: function() {
deprecation.warn(
'Blockly.mainWorkspace', 'August 2022', 'September 2022',
'Blockly.mainWorkspace', 'version 9', 'version 10',
'Blockly.getMainWorkspace');
return common.getMainWorkspace();
},
@@ -130,14 +130,12 @@ Object.defineProperties(Blockly, {
selected: {
get: function() {
deprecation.warn(
'Blockly.selected', 'August 2022', 'September 2022',
'Blockly.getSelected');
'Blockly.selected', 'version 9', 'version 10', 'Blockly.getSelected');
return common.getSelected();
},
set: function(newSelection) {
deprecation.warn(
'Blockly.selected', 'August 2022', 'September 2022',
'Blockly.getSelected');
'Blockly.selected', 'version 9', 'version 10', 'Blockly.getSelected');
common.setSelected(newSelection);
},
},

View File

@@ -16,6 +16,7 @@ import * as browserEvents from './browser_events.js';
import type {MenuItem} from './menuitem.js';
import * as aria from './utils/aria.js';
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import {KeyCodes} from './utils/keycodes.js';
import type {Size} from './utils/size.js';
import * as style from './utils/style.js';
@@ -32,7 +33,7 @@ export class Menu {
* (Nulls are never in the array, but typing the array as nullable prevents
* the compiler from objecting to .indexOf(null))
*/
private readonly menuItems_: MenuItem[] = [];
private readonly menuItems: MenuItem[] = [];
/**
* Coordinates of the mousedown event that caused this menu to open. Used to
@@ -45,28 +46,28 @@ export class Menu {
* This is the element that we will listen to the real focus events on.
* A value of null means no menu item is highlighted.
*/
private highlightedItem_: MenuItem|null = null;
private highlightedItem: MenuItem|null = null;
/** Mouse over event data. */
private mouseOverHandler_: browserEvents.Data|null = null;
private mouseOverHandler: browserEvents.Data|null = null;
/** Click event data. */
private clickHandler_: browserEvents.Data|null = null;
private clickHandler: browserEvents.Data|null = null;
/** Mouse enter event data. */
private mouseEnterHandler_: browserEvents.Data|null = null;
private mouseEnterHandler: browserEvents.Data|null = null;
/** Mouse leave event data. */
private mouseLeaveHandler_: browserEvents.Data|null = null;
private mouseLeaveHandler: browserEvents.Data|null = null;
/** Key down event data. */
private onKeyDownHandler_: browserEvents.Data|null = null;
private onKeyDownHandler: browserEvents.Data|null = null;
/** The menu's root DOM element. */
private element_: HTMLDivElement|null = null;
private element: HTMLDivElement|null = null;
/** ARIA name for this menu. */
private roleName_: aria.Role|null = null;
private roleName: aria.Role|null = null;
/** Constructs a new Menu instance. */
constructor() {}
@@ -78,7 +79,7 @@ export class Menu {
* @internal
*/
addChild(menuItem: MenuItem) {
this.menuItems_.push(menuItem);
this.menuItems.push(menuItem);
}
/**
@@ -92,27 +93,27 @@ export class Menu {
// goog-menu is deprecated, use blocklyMenu. May 2020.
element.className = 'blocklyMenu goog-menu blocklyNonSelectable';
element.tabIndex = 0;
if (this.roleName_) {
aria.setRole(element, this.roleName_);
if (this.roleName) {
aria.setRole(element, this.roleName);
}
this.element_ = element;
this.element = element;
// Add menu items.
for (let i = 0, menuItem; menuItem = this.menuItems_[i]; i++) {
for (let i = 0, menuItem; menuItem = this.menuItems[i]; i++) {
element.appendChild(menuItem.createDom());
}
// Add event handlers.
this.mouseOverHandler_ = browserEvents.conditionalBind(
element, 'mouseover', this, this.handleMouseOver_, true);
this.clickHandler_ = browserEvents.conditionalBind(
element, 'click', this, this.handleClick_, true);
this.mouseEnterHandler_ = browserEvents.conditionalBind(
element, 'mouseenter', this, this.handleMouseEnter_, true);
this.mouseLeaveHandler_ = browserEvents.conditionalBind(
element, 'mouseleave', this, this.handleMouseLeave_, true);
this.onKeyDownHandler_ = browserEvents.conditionalBind(
element, 'keydown', this, this.handleKeyEvent_);
this.mouseOverHandler = browserEvents.conditionalBind(
element, 'mouseover', this, this.handleMouseOver, true);
this.clickHandler = browserEvents.conditionalBind(
element, 'click', this, this.handleClick, true);
this.mouseEnterHandler = browserEvents.conditionalBind(
element, 'mouseenter', this, this.handleMouseEnter, true);
this.mouseLeaveHandler = browserEvents.conditionalBind(
element, 'mouseleave', this, this.handleMouseLeave, true);
this.onKeyDownHandler = browserEvents.conditionalBind(
element, 'keydown', this, this.handleKeyEvent);
container.appendChild(element);
return element;
@@ -125,7 +126,7 @@ export class Menu {
* @internal
*/
getElement(): HTMLDivElement|null {
return this.element_;
return this.element;
}
/**
@@ -137,16 +138,16 @@ export class Menu {
const el = this.getElement();
if (el) {
el.focus({preventScroll: true});
el.classList.add('blocklyFocused');
dom.addClass(el, 'blocklyFocused');
}
}
/** Blur the menu element. */
private blur_() {
private blur() {
const el = this.getElement();
if (el) {
el.blur();
el.classList.remove('blocklyFocused');
dom.removeClass(el, 'blocklyFocused');
}
}
@@ -157,38 +158,38 @@ export class Menu {
* @internal
*/
setRole(roleName: aria.Role) {
this.roleName_ = roleName;
this.roleName = roleName;
}
/** Dispose of this menu. */
dispose() {
// Remove event handlers.
if (this.mouseOverHandler_) {
browserEvents.unbind(this.mouseOverHandler_);
this.mouseOverHandler_ = null;
if (this.mouseOverHandler) {
browserEvents.unbind(this.mouseOverHandler);
this.mouseOverHandler = null;
}
if (this.clickHandler_) {
browserEvents.unbind(this.clickHandler_);
this.clickHandler_ = null;
if (this.clickHandler) {
browserEvents.unbind(this.clickHandler);
this.clickHandler = null;
}
if (this.mouseEnterHandler_) {
browserEvents.unbind(this.mouseEnterHandler_);
this.mouseEnterHandler_ = null;
if (this.mouseEnterHandler) {
browserEvents.unbind(this.mouseEnterHandler);
this.mouseEnterHandler = null;
}
if (this.mouseLeaveHandler_) {
browserEvents.unbind(this.mouseLeaveHandler_);
this.mouseLeaveHandler_ = null;
if (this.mouseLeaveHandler) {
browserEvents.unbind(this.mouseLeaveHandler);
this.mouseLeaveHandler = null;
}
if (this.onKeyDownHandler_) {
browserEvents.unbind(this.onKeyDownHandler_);
this.onKeyDownHandler_ = null;
if (this.onKeyDownHandler) {
browserEvents.unbind(this.onKeyDownHandler);
this.onKeyDownHandler = null;
}
// Remove menu items.
for (let i = 0, menuItem; menuItem = this.menuItems_[i]; i++) {
for (let i = 0, menuItem; menuItem = this.menuItems[i]; i++) {
menuItem.dispose();
}
this.element_ = null;
this.element = null;
}
// Child component management.
@@ -200,7 +201,7 @@ export class Menu {
* @param elem DOM element whose owner is to be returned.
* @returns Menu item for which the DOM element belongs to.
*/
private getMenuItem_(elem: Element): MenuItem|null {
private getMenuItem(elem: Element): MenuItem|null {
const menuElem = this.getElement();
// Node might be the menu border (resulting in no associated menu item), or
// a menu item's div, or some element within the menu item.
@@ -210,7 +211,7 @@ export class Menu {
while (currentElement && currentElement !== menuElem) {
if (currentElement.classList.contains('blocklyMenuItem')) {
// Having found a menu item's div, locate that menu item in this menu.
for (let i = 0, menuItem; menuItem = this.menuItems_[i]; i++) {
for (let i = 0, menuItem; menuItem = this.menuItems[i]; i++) {
if (menuItem.getElement() === currentElement) {
return menuItem;
}
@@ -230,14 +231,14 @@ export class Menu {
* @internal
*/
setHighlighted(item: MenuItem|null) {
const currentHighlighted = this.highlightedItem_;
const currentHighlighted = this.highlightedItem;
if (currentHighlighted) {
currentHighlighted.setHighlighted(false);
this.highlightedItem_ = null;
this.highlightedItem = null;
}
if (item) {
item.setHighlighted(true);
this.highlightedItem_ = item;
this.highlightedItem = item;
// Bring the highlighted item into view. This has no effect if the menu is
// not scrollable.
const el = this.getElement() as Element;
@@ -254,10 +255,10 @@ export class Menu {
* @internal
*/
highlightNext() {
const index = this.highlightedItem_ ?
this.menuItems_.indexOf(this.highlightedItem_) :
const index = this.highlightedItem ?
this.menuItems.indexOf(this.highlightedItem) :
-1;
this.highlightHelper_(index, 1);
this.highlightHelper(index, 1);
}
/**
@@ -267,20 +268,20 @@ export class Menu {
* @internal
*/
highlightPrevious() {
const index = this.highlightedItem_ ?
this.menuItems_.indexOf(this.highlightedItem_) :
const index = this.highlightedItem ?
this.menuItems.indexOf(this.highlightedItem) :
-1;
this.highlightHelper_(index < 0 ? this.menuItems_.length : index, -1);
this.highlightHelper(index < 0 ? this.menuItems.length : index, -1);
}
/** Highlights the first highlightable item. */
private highlightFirst_() {
this.highlightHelper_(-1, 1);
private highlightFirst() {
this.highlightHelper(-1, 1);
}
/** Highlights the last highlightable item. */
private highlightLast_() {
this.highlightHelper_(this.menuItems_.length, -1);
private highlightLast() {
this.highlightHelper(this.menuItems.length, -1);
}
/**
@@ -290,10 +291,10 @@ export class Menu {
* @param startIndex Start index.
* @param delta Step direction: 1 to go down, -1 to go up.
*/
private highlightHelper_(startIndex: number, delta: number) {
private highlightHelper(startIndex: number, delta: number) {
let index = startIndex + delta;
let menuItem;
while (menuItem = this.menuItems_[index]) {
while (menuItem = this.menuItems[index]) {
if (menuItem.isEnabled()) {
this.setHighlighted(menuItem);
break;
@@ -309,12 +310,12 @@ export class Menu {
*
* @param e Mouse event to handle.
*/
private handleMouseOver_(e: Event) {
const menuItem = this.getMenuItem_(e.target as Element);
private handleMouseOver(e: Event) {
const menuItem = this.getMenuItem(e.target as Element);
if (menuItem) {
if (menuItem.isEnabled()) {
if (this.highlightedItem_ !== menuItem) {
if (this.highlightedItem !== menuItem) {
this.setHighlighted(menuItem);
}
} else {
@@ -328,7 +329,7 @@ export class Menu {
*
* @param e Click event to handle.
*/
private handleClick_(e: Event) {
private handleClick(e: Event) {
const oldCoords = this.openingCoords;
// Clear out the saved opening coords immediately so they're not used twice.
this.openingCoords = null;
@@ -350,7 +351,7 @@ export class Menu {
}
}
const menuItem = this.getMenuItem_(e.target as Element);
const menuItem = this.getMenuItem(e.target as Element);
if (menuItem) {
menuItem.performAction();
}
@@ -361,7 +362,7 @@ export class Menu {
*
* @param _e Mouse event to handle.
*/
private handleMouseEnter_(_e: Event) {
private handleMouseEnter(_e: Event) {
this.focus();
}
@@ -370,9 +371,9 @@ export class Menu {
*
* @param _e Mouse event to handle.
*/
private handleMouseLeave_(_e: Event) {
private handleMouseLeave(_e: Event) {
if (this.getElement()) {
this.blur_();
this.blur();
this.setHighlighted(null);
}
}
@@ -386,27 +387,20 @@ export class Menu {
*
* @param e Key event to handle.
*/
private handleKeyEvent_(e: Event) {
if (!this.menuItems_.length) {
private handleKeyEvent(e: Event) {
if (!this.menuItems.length) {
// Empty menu.
return;
}
// AnyDuringMigration because: Property 'altKey' does not exist on type
// 'Event'. AnyDuringMigration because: Property 'metaKey' does not exist
// on type 'Event'. AnyDuringMigration because: Property 'ctrlKey' does not
// exist on type 'Event'. AnyDuringMigration because: Property 'shiftKey'
// does not exist on type 'Event'.
if ((e as AnyDuringMigration).shiftKey ||
(e as AnyDuringMigration).ctrlKey ||
(e as AnyDuringMigration).metaKey || (e as AnyDuringMigration).altKey) {
const keyboardEvent = e as KeyboardEvent;
if (keyboardEvent.shiftKey || keyboardEvent.ctrlKey ||
keyboardEvent.metaKey || keyboardEvent.altKey) {
// Do not handle the key event if any modifier key is pressed.
return;
}
const highlighted = this.highlightedItem_;
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
switch ((e as AnyDuringMigration).keyCode) {
const highlighted = this.highlightedItem;
switch (keyboardEvent.keyCode) {
case KeyCodes.ENTER:
case KeyCodes.SPACE:
if (highlighted) {
@@ -424,12 +418,12 @@ export class Menu {
case KeyCodes.PAGE_UP:
case KeyCodes.HOME:
this.highlightFirst_();
this.highlightFirst();
break;
case KeyCodes.PAGE_DOWN:
case KeyCodes.END:
this.highlightLast_();
this.highlightLast();
break;
default:
@@ -448,10 +442,10 @@ export class Menu {
* @internal
*/
getSize(): Size {
const menuDom = this.getElement();
const menuSize = style.getSize(menuDom as Element);
const menuDom = this.getElement() as HTMLDivElement;
const menuSize = style.getSize(menuDom);
// Recalculate height for the total content, not only box height.
menuSize.height = menuDom!.scrollHeight;
menuSize.height = menuDom.scrollHeight;
return menuSize;
}
}

View File

@@ -13,6 +13,7 @@ import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.MenuItem');
import * as aria from './utils/aria.js';
import * as dom from './utils/dom.js';
import * as idGenerator from './utils/idgenerator.js';
@@ -23,28 +24,28 @@ import * as idGenerator from './utils/idgenerator.js';
*/
export class MenuItem {
/** Is the menu item clickable, as opposed to greyed-out. */
private enabled_ = true;
private enabled = true;
/** The DOM element for the menu item. */
private element_: HTMLDivElement|null = null;
private element: HTMLDivElement|null = null;
/** Whether the menu item is rendered right-to-left. */
private rightToLeft_ = false;
private rightToLeft = false;
/** ARIA name for this menu. */
private roleName_: aria.Role|null = null;
private roleName: aria.Role|null = null;
/** Is this menu item checkable. */
private checkable_ = false;
private checkable = false;
/** Is this menu item currently checked. */
private checked_ = false;
private checked = false;
/** Is this menu item currently highlighted. */
private highlight_ = false;
private highlight = false;
/** Bound function to call when this menu item is clicked. */
private actionHandler_: Function|null = null;
private actionHandler: Function|null = null;
/**
* @param content Text caption to display as the content of the item, or a
@@ -63,22 +64,22 @@ export class MenuItem {
createDom(): Element {
const element = (document.createElement('div'));
element.id = idGenerator.getNextUniqueId();
this.element_ = element;
this.element = element;
// Set class and style
// goog-menuitem* is deprecated, use blocklyMenuItem*. May 2020.
element.className = 'blocklyMenuItem goog-menuitem ' +
(this.enabled_ ? '' :
'blocklyMenuItemDisabled goog-menuitem-disabled ') +
(this.checked_ ? 'blocklyMenuItemSelected goog-option-selected ' : '') +
(this.highlight_ ? 'blocklyMenuItemHighlight goog-menuitem-highlight ' :
'') +
(this.rightToLeft_ ? 'blocklyMenuItemRtl goog-menuitem-rtl ' : '');
(this.enabled ? '' :
'blocklyMenuItemDisabled goog-menuitem-disabled ') +
(this.checked ? 'blocklyMenuItemSelected goog-option-selected ' : '') +
(this.highlight ? 'blocklyMenuItemHighlight goog-menuitem-highlight ' :
'') +
(this.rightToLeft ? 'blocklyMenuItemRtl goog-menuitem-rtl ' : '');
const content = (document.createElement('div'));
content.className = 'blocklyMenuItemContent goog-menuitem-content';
// Add a checkbox for checkable menu items.
if (this.checkable_) {
if (this.checkable) {
const checkbox = (document.createElement('div'));
checkbox.className = 'blocklyMenuItemCheckbox goog-menuitem-checkbox';
content.appendChild(checkbox);
@@ -92,20 +93,19 @@ export class MenuItem {
element.appendChild(content);
// Initialize ARIA role and state.
if (this.roleName_) {
aria.setRole(element, this.roleName_);
if (this.roleName) {
aria.setRole(element, this.roleName);
}
aria.setState(
element, aria.State.SELECTED,
this.checkable_ && this.checked_ || false);
aria.setState(element, aria.State.DISABLED, !this.enabled_);
element, aria.State.SELECTED, this.checkable && this.checked || false);
aria.setState(element, aria.State.DISABLED, !this.enabled);
return element;
}
/** Dispose of this menu item. */
dispose() {
this.element_ = null;
this.element = null;
}
/**
@@ -115,7 +115,7 @@ export class MenuItem {
* @internal
*/
getElement(): Element|null {
return this.element_;
return this.element;
}
/**
@@ -125,7 +125,7 @@ export class MenuItem {
* @internal
*/
getId(): string {
return this.element_!.id;
return this.element!.id;
}
/**
@@ -145,7 +145,7 @@ export class MenuItem {
* @internal
*/
setRightToLeft(rtl: boolean) {
this.rightToLeft_ = rtl;
this.rightToLeft = rtl;
}
/**
@@ -155,7 +155,7 @@ export class MenuItem {
* @internal
*/
setRole(roleName: aria.Role) {
this.roleName_ = roleName;
this.roleName = roleName;
}
/**
@@ -166,7 +166,7 @@ export class MenuItem {
* @internal
*/
setCheckable(checkable: boolean) {
this.checkable_ = checkable;
this.checkable = checkable;
}
/**
@@ -176,7 +176,7 @@ export class MenuItem {
* @internal
*/
setChecked(checked: boolean) {
this.checked_ = checked;
this.checked = checked;
}
/**
@@ -186,7 +186,7 @@ export class MenuItem {
* @internal
*/
setHighlighted(highlight: boolean) {
this.highlight_ = highlight;
this.highlight = highlight;
const el = this.getElement();
if (el && this.isEnabled()) {
@@ -195,11 +195,11 @@ export class MenuItem {
const name = 'blocklyMenuItemHighlight';
const nameDep = 'goog-menuitem-highlight';
if (highlight) {
el.classList.add(name);
el.classList.add(nameDep);
dom.addClass(el, name);
dom.addClass(el, nameDep);
} else {
el.classList.remove(name);
el.classList.remove(nameDep);
dom.removeClass(el, name);
dom.removeClass(el, nameDep);
}
}
}
@@ -211,7 +211,7 @@ export class MenuItem {
* @internal
*/
isEnabled(): boolean {
return this.enabled_;
return this.enabled;
}
/**
@@ -221,7 +221,7 @@ export class MenuItem {
* @internal
*/
setEnabled(enabled: boolean) {
this.enabled_ = enabled;
this.enabled = enabled;
}
/**
@@ -231,8 +231,8 @@ export class MenuItem {
* @internal
*/
performAction() {
if (this.isEnabled() && this.actionHandler_) {
this.actionHandler_(this);
if (this.isEnabled() && this.actionHandler) {
this.actionHandler(this);
}
}
@@ -245,6 +245,6 @@ export class MenuItem {
* @internal
*/
onAction(fn: (p1: MenuItem) => void, obj: object) {
this.actionHandler_ = fn.bind(obj);
this.actionHandler = fn.bind(obj);
}
}

View File

@@ -15,3 +15,20 @@ goog.declareModuleId('Blockly.Msg');
/** A dictionary of localised messages. */
export const Msg: {[key: string]: string} = Object.create(null);
/**
* Sets the locale (i.e. the localized messages/block-text/etc) to the given
* locale.
*
* This is not useful/necessary when loading from a script tag, because the
* messages are automatically cluged into the Blockly.Msg object. But we provide
* it in both the script-tag and non-script-tag contexts so that the tscompiler
* can properly create our type definition files.
*
* @param locale An object defining the messages for a given language.
*/
export const setLocale = function(locale: {[key: string]: string}) {
Object.keys(locale).forEach(function(k) {
Msg[k] = locale[k];
});
};

View File

@@ -32,6 +32,7 @@ import * as dom from './utils/dom.js';
import {Svg} from './utils/svg.js';
import * as toolbox from './utils/toolbox.js';
import * as xml from './utils/xml.js';
import * as deprecation from './utils/deprecation.js';
import type {WorkspaceSvg} from './workspace_svg.js';
@@ -77,8 +78,14 @@ export class Mutator extends Icon {
private updateWorkspacePid_: ReturnType<typeof setTimeout>|null = null;
/** @param quarkNames List of names of sub-blocks for flyout. */
constructor(block: BlockSvg, quarkNames: string[]) {
super(block);
constructor(quarkNames: string[], block?: BlockSvg) {
if (!block) {
deprecation.warn(
'Calling the Mutator constructor without passing the block it is attached to',
'version 9', 'version 10',
'the constructor by passing the list of subblocks and the block instance to attach the mutator to');
}
super(block ?? null);
this.quarkNames_ = quarkNames;
}
@@ -145,7 +152,7 @@ export class Mutator extends Icon {
* @param e Mouse click event.
*/
protected override iconClick_(e: MouseEvent) {
if (this.block_.isEditable()) {
if (this.getBlock().isEditable()) {
super.iconClick_(e);
}
}
@@ -175,19 +182,20 @@ export class Mutator extends Icon {
} else {
quarkXml = null;
}
const block = this.getBlock();
const workspaceOptions = new Options(({
// If you want to enable disabling, also remove the
// event filter from workspaceChanged_ .
'disable': false,
'parentWorkspace': this.block_.workspace,
'media': this.block_.workspace.options.pathToMedia,
'rtl': this.block_.RTL,
'parentWorkspace': block.workspace,
'media': block.workspace.options.pathToMedia,
'rtl': block.RTL,
'horizontalLayout': false,
'renderer': this.block_.workspace.options.renderer,
'rendererOverrides': this.block_.workspace.options.rendererOverrides,
'renderer': block.workspace.options.renderer,
'rendererOverrides': block.workspace.options.rendererOverrides,
} as BlocklyOptions));
workspaceOptions.toolboxPosition =
this.block_.RTL ? toolbox.Position.RIGHT : toolbox.Position.LEFT;
block.RTL ? toolbox.Position.RIGHT : toolbox.Position.LEFT;
const hasFlyout = !!quarkXml;
if (hasFlyout) {
workspaceOptions.languageTree = toolbox.convertToolboxDefToJson(quarkXml);
@@ -227,16 +235,16 @@ export class Mutator extends Icon {
/** Add or remove the UI indicating if this icon may be clicked or not. */
override updateEditable() {
super.updateEditable();
if (!this.block_.isInFlyout) {
if (this.block_.isEditable()) {
if (!this.getBlock().isInFlyout) {
if (this.getBlock().isEditable()) {
if (this.iconGroup_) {
this.iconGroup_.classList.remove('blocklyIconGroupReadonly');
dom.removeClass(this.iconGroup_, 'blocklyIconGroupReadonly');
}
} else {
// Close any mutator bubble. Icon is not clickable.
this.setVisible(false);
if (this.iconGroup_) {
this.iconGroup_.classList.add('blocklyIconGroupReadonly');
dom.addClass(this.iconGroup_, 'blocklyIconGroupReadonly');
}
}
}
@@ -255,7 +263,7 @@ export class Mutator extends Icon {
height = Math.max(height, flyoutScrollMetrics.height + 20);
width += flyout.getWidth();
}
if (this.block_.RTL) {
if (this.getBlock().RTL) {
width = -workspaceSize.x;
}
width += doubleBorderWidth * 3;
@@ -275,7 +283,7 @@ export class Mutator extends Icon {
this.workspaceWidth_, this.workspaceHeight_);
}
if (this.block_.RTL) {
if (this.getBlock().RTL) {
// Scroll the workspace to always left-align.
const translation = 'translate(' + this.workspaceWidth_ + ',0)';
this.workspace_!.getCanvas().setAttribute('transform', translation);
@@ -300,16 +308,16 @@ export class Mutator extends Icon {
// No change.
return;
}
const block = this.getBlock();
eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
this.block_, visible, 'mutator'));
block, visible, 'mutator'));
if (visible) {
// Create the bubble.
this.bubble_ = new Bubble(
(this.block_.workspace as WorkspaceSvg), this.createEditor_(),
this.block_.pathObject.svgPath, (this.iconXY_ as Coordinate), null,
null);
block.workspace, this.createEditor_(), block.pathObject.svgPath,
(this.iconXY_ as Coordinate), null, null);
// Expose this mutator's block's ID on its top-level SVG group.
this.bubble_.setSvgId(this.block_.id);
this.bubble_.setSvgId(block.id);
this.bubble_.registerMoveEvent(this.onBubbleMove_.bind(this));
const tree = this.workspace_!.options.languageTree;
const flyout = this.workspace_!.getFlyout();
@@ -318,7 +326,7 @@ export class Mutator extends Icon {
flyout!.show(tree);
}
this.rootBlock_ = this.block_!.decompose!(this.workspace_!)!;
this.rootBlock_ = block.decompose!(this.workspace_!)!;
const blocks = this.rootBlock_!.getDescendants(false);
for (let i = 0, child; child = blocks[i]; i++) {
child.render();
@@ -335,20 +343,21 @@ export class Mutator extends Icon {
margin = 16;
x = margin;
}
if (this.block_.RTL) {
if (block.RTL) {
x = -x;
}
this.rootBlock_!.moveBy(x, margin);
// Save the initial connections, then listen for further changes.
if (this.block_.saveConnections) {
if (block.saveConnections) {
const thisRootBlock = this.rootBlock_;
this.block_.saveConnections(thisRootBlock);
block.saveConnections(thisRootBlock);
this.sourceListener_ = () => {
if (this.block_ && this.block_.saveConnections) {
this.block_.saveConnections(thisRootBlock);
const currentBlock = this.getBlock();
if (currentBlock.saveConnections) {
currentBlock.saveConnections(thisRootBlock);
}
};
this.block_.workspace.addChangeListener(this.sourceListener_);
block.workspace.addChangeListener(this.sourceListener_);
}
this.resizeBubble_();
// When the mutator's workspace changes, update the source block.
@@ -367,7 +376,7 @@ export class Mutator extends Icon {
this.workspaceWidth_ = 0;
this.workspaceHeight_ = 0;
if (this.sourceListener_) {
this.block_.workspace.removeChangeListener(this.sourceListener_);
block.workspace.removeChangeListener(this.sourceListener_);
this.sourceListener_ = null;
}
}
@@ -438,7 +447,7 @@ export class Mutator extends Icon {
if (!existingGroup) {
eventUtils.setGroup(true);
}
const block = this.block_ as BlockSvg;
const block = this.getBlock();
const oldExtraState = BlockChange.getExtraBlockState_(block);
// Switch off rendering while the source block is rebuilt.
@@ -482,7 +491,7 @@ export class Mutator extends Icon {
/** Dispose of this mutator. */
override dispose() {
this.block_.mutator = null;
this.getBlock().mutator = null;
super.dispose();
}

View File

@@ -22,9 +22,12 @@ import * as common from './common.js';
import type {Abstract} from './events/events_abstract.js';
import type {BubbleOpen} from './events/events_bubble_open.js';
import * as eventUtils from './events/utils.js';
import type {Field} from './field.js';
import {Field, UnattachedFieldError} from './field.js';
import {Msg} from './msg.js';
import {Names} from './names.js';
import {ObservableProcedureMap} from './procedures/observable_procedure_map.js';
import {ObservableProcedureModel} from './procedures/observable_procedure_model.js';
import {ObservableParameterModel} from './procedures/observable_parameter_model.js';
import * as utilsXml from './utils/xml.js';
import * as Variables from './variables.js';
import type {Workspace} from './workspace.js';
@@ -180,14 +183,19 @@ export function isNameUsed(
* @alias Blockly.Procedures.rename
*/
export function rename(this: Field, name: string): string {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
// Strip leading and trailing whitespace. Beyond this, all names are legal.
name = name.trim();
const legalName = findLegalName(name, (this.getSourceBlock()));
const legalName = findLegalName(name, block);
const oldName = this.getValue();
if (oldName !== name && oldName !== legalName) {
// Rename any callers.
const blocks = this.getSourceBlock().workspace.getAllBlocks(false);
const blocks = block.workspace.getAllBlocks(false);
for (let i = 0; i < blocks.length; i++) {
// Assume it is a procedure so we can check.
const procedureBlock = blocks[i] as unknown as ProcedureBlock;
@@ -445,3 +453,9 @@ export function getDefinition(name: string, workspace: Workspace): Block|null {
}
return null;
}
export {
ObservableProcedureMap,
ObservableProcedureModel,
ObservableParameterModel,
};

View File

@@ -0,0 +1,79 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {genUid} from '../utils/idgenerator.js';
import type {IParameterModel} from '../interfaces/i_parameter_model.js';
import {triggerProceduresUpdate} from './update_procedures.js';
import type {VariableModel} from '../variable_model.js';
import type {Workspace} from '../workspace.js';
export class ObservableParameterModel implements IParameterModel {
private id: string;
private variable: VariableModel;
constructor(
private readonly workspace: Workspace, name: string, id?: string) {
this.id = id ?? genUid();
this.variable =
this.workspace.getVariable(name) ?? workspace.createVariable(name);
}
/**
* Sets the name of this parameter to the given name.
*/
setName(name: string): this {
// TODO(#6516): Fire events.
if (name == this.variable.name) return this;
this.variable =
this.workspace.getVariable(name) ?? this.workspace.createVariable(name);
triggerProceduresUpdate(this.workspace);
return this;
}
/**
* Unimplemented. The built-in ParameterModel does not support typing.
* If you want your procedure blocks to have typed parameters, you need to
* implement your own ParameterModel.
*
* @throws Throws for the ObservableParameterModel specifically because this
* method is unimplemented.
*/
setTypes(_types: string[]): this {
throw new Error(
'The built-in ParameterModel does not support typing. You need to ' +
'implement your own custom ParameterModel.');
}
/**
* Returns the name of this parameter.
*/
getName(): string {
return this.variable.name;
}
/**
* Returns the types of this parameter.
*/
getTypes(): string[] {
return [];
}
/**
* Returns the unique language-neutral ID for the parameter.
*
* This represents the identify of the variable model which does not change
* over time.
*/
getId(): string {
return this.id;
}
/** Returns the variable model associated with the parameter model. */
getVariableModel(): VariableModel {
return this.variable;
}
}

View File

@@ -0,0 +1,64 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import {triggerProceduresUpdate} from './update_procedures.js';
import type {Workspace} from '../workspace.js';
import {IProcedureMap} from '../interfaces/i_procedure_map.js';
export class ObservableProcedureMap extends
Map<string, IProcedureModel> implements IProcedureMap {
constructor(private readonly workspace: Workspace) {
super();
}
/**
* Adds the given procedure model to the procedure map.
*/
override set(id: string, proc: IProcedureModel): this {
// TODO(#6516): Fire events.
super.set(id, proc);
return this;
}
/**
* Deletes the ProcedureModel with the given ID from the procedure map (if it
* exists).
*/
override delete(id: string): boolean {
// TODO(#6516): Fire events.
const existed = super.delete(id);
triggerProceduresUpdate(this.workspace);
return existed;
}
/**
* Removes all ProcedureModels from the procedure map.
*/
override clear() {
// TODO(#6516): Fire events.
super.clear();
triggerProceduresUpdate(this.workspace);
}
/**
* Adds the given ProcedureModel to the map of procedure models, so that
* blocks can find it.
*/
add(proc: IProcedureModel): this {
// TODO(#6516): Fire events.
// TODO(#6526): See if this method is actually useful.
return this.set(proc.getId(), proc);
}
/**
* Returns all of the procedures stored in this map.
*/
getProcedures(): IProcedureModel[] {
return [...this.values()];
}
}

View File

@@ -0,0 +1,123 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {genUid} from '../utils/idgenerator.js';
import type {IParameterModel} from '../interfaces/i_parameter_model.js';
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import {triggerProceduresUpdate} from './update_procedures.js';
import type {Workspace} from '../workspace.js';
export class ObservableProcedureModel implements IProcedureModel {
private id: string;
private name: string;
private parameters: IParameterModel[] = [];
private returnTypes: string[]|null = null;
private enabled = true;
constructor(
private readonly workspace: Workspace, name: string, id?: string) {
this.id = id ?? genUid();
this.name = name;
}
/** Sets the human-readable name of the procedure. */
setName(name: string): this {
// TODO(#6516): Fire events.
this.name = name;
triggerProceduresUpdate(this.workspace);
return this;
}
/**
* Inserts a parameter into the list of parameters.
*
* To move a parameter, first delete it, and then re-insert.
*/
insertParameter(parameterModel: IParameterModel, index: number): this {
// TODO(#6516): Fire events.
this.parameters.splice(index, 0, parameterModel);
triggerProceduresUpdate(this.workspace);
return this;
}
/** Removes the parameter at the given index from the parameter list. */
deleteParameter(index: number): this {
// TODO(#6516): Fire events.
this.parameters.splice(index, 1);
triggerProceduresUpdate(this.workspace);
return this;
}
/**
* Sets whether the procedure has a return value (empty array) or no return
* value (null).
*
* The built-in procedure model does not support procedures that have actual
* return types (i.e. non-empty arrays, e.g. ['number']). If you want your
* procedure block to have return types, you need to implement your own
* procedure model.
*/
setReturnTypes(types: string[]|null): this {
if (types && types.length) {
throw new Error(
'The built-in ProcedureModel does not support typing. You need to ' +
'implement your own custom ProcedureModel.');
}
this.returnTypes = types;
// TODO(#6516): Fire events.
triggerProceduresUpdate(this.workspace);
return this;
}
/**
* Sets whether this procedure is enabled/disabled. If a procedure is disabled
* all procedure caller blocks should be disabled as well.
*/
setEnabled(enabled: boolean): this {
// TODO(#6516): Fire events.
this.enabled = enabled;
triggerProceduresUpdate(this.workspace);
return this;
}
/** Returns the unique language-neutral ID for the procedure. */
getId(): string {
return this.id;
}
/** Returns the human-readable name of the procedure. */
getName(): string {
return this.name;
}
/** Returns the parameter at the given index in the parameter list. */
getParameter(index: number): IParameterModel {
return this.parameters[index];
}
/** Returns an array of all of the parameters in the parameter list. */
getParameters(): IParameterModel[] {
return [...this.parameters];
}
/**
* Returns the return type of the procedure.
*
* Null represents a procedure that does not return a value.
*/
getReturnTypes(): string[]|null {
return this.returnTypes;
}
/**
* Returns whether the procedure is enabled/disabled. If a procedure is
* disabled, all procedure caller blocks should be disabled as well.
*/
getEnabled(): boolean {
return this.enabled;
}
}

View File

@@ -0,0 +1,22 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {isProcedureBlock} from '../interfaces/i_procedure_block.js';
import {Workspace} from '../workspace.js';
/**
* Calls the `doProcedureUpdate` method on all blocks which implement it.
*
* @internal
*/
export function triggerProceduresUpdate(workspace: Workspace) {
for (const block of workspace.getAllBlocks(false)) {
if (isProcedureBlock(block)) {
block.doProcedureUpdate();
}
}
}

View File

@@ -280,6 +280,10 @@ export class RenderedConnection extends Connection {
/** Add highlighting around this connection. */
highlight() {
if (this.highlightPath) {
// This connection is already highlighted
return;
}
let steps;
const sourceBlockSvg = (this.sourceBlock_);
const renderConstants =
@@ -495,7 +499,7 @@ export class RenderedConnection extends Connection {
* @returns List of connections.
* @internal
*/
override neighbours(maxLimit: number): Connection[] {
override neighbours(maxLimit: number): RenderedConnection[] {
return this.dbOpposite_.getNeighbours(this, maxLimit);
}

View File

@@ -48,23 +48,6 @@ import {MarkerSvg} from './marker_svg.js';
import {PathObject} from './path_object.js';
import {Renderer} from './renderer.js';
/**
* Returns whether the debugger is turned on.
*
* @returns Whether the debugger is turned on.
* @alias Blockly.blockRendering.isDebuggerEnabled
* @deprecated
* @internal
*/
export function isDebuggerEnabled(): boolean {
deprecation.warn(
'Blockly.blockRendering.isDebuggerEnabled()', 'September 2021',
'September 2022',
'the debug renderer in @blockly/dev-tools (See https://www.npmjs.com/package/@blockly/dev-tools.)');
return debug.isDebuggerEnabled();
}
/**
* Registers a new renderer.
*
@@ -86,26 +69,12 @@ export function unregister(name: string) {
registry.unregister(registry.Type.RENDERER, name);
}
/**
* Turn on the blocks debugger.
*
* @alias Blockly.blockRendering.startDebugger
* @deprecated
* @internal
*/
export function startDebugger() {
deprecation.warn(
'Blockly.blockRendering.startDebugger()', 'September 2021',
'September 2022',
'the debug renderer in @blockly/dev-tools (See https://www.npmjs.com/package/@blockly/dev-tools.)');
debug.startDebugger();
}
/**
* Turn off the blocks debugger.
*
* @alias Blockly.blockRendering.stopDebugger
* @deprecated
* @deprecated Use the debug renderer in **\@blockly/dev-tools** (See {@link
* https://www.npmjs.com/package/@blockly/dev-tools}.)
* @internal
*/
export function stopDebugger() {

View File

@@ -563,7 +563,7 @@ export class ConstantProvider {
this.setComponentConstants_(theme);
this.ADD_START_HATS =
theme.startHats !== null ? theme.startHats : this.ADD_START_HATS;
theme.startHats !== undefined ? theme.startHats : this.ADD_START_HATS;
}
/**
@@ -658,12 +658,7 @@ export class ConstantProvider {
* @param blockStyle A full or partial block style object.
* @returns A full block style object, with all required properties populated.
*/
protected validatedBlockStyle_(blockStyle: {
colourPrimary: string,
colourSecondary?: string,
colourTertiary?: string,
hat?: string
}): BlockStyle {
protected validatedBlockStyle_(blockStyle: Partial<BlockStyle>): BlockStyle {
// Make a new object with all of the same properties.
const valid = {} as BlockStyle;
if (blockStyle) {

View File

@@ -38,8 +38,7 @@ export function isDebuggerEnabled(): boolean {
*/
export function startDebugger() {
deprecation.warn(
'Blockly.blockRendering.debug.startDebugger()', 'February 2022',
'September 2022',
'Blockly.blockRendering.debug.startDebugger()', 'version 8', 'version 10',
'the debug renderer in @blockly/dev-tools (See https://www.npmjs.com/package/@blockly/dev-tools.)');
useDebugger = true;
}
@@ -54,8 +53,7 @@ export function startDebugger() {
*/
export function stopDebugger() {
deprecation.warn(
'Blockly.blockRendering.debug.stopDebugger()', 'February 2022',
'September 2022',
'Blockly.blockRendering.debug.stopDebugger()', 'version 8', 'version 10',
'the debug renderer in @blockly/dev-tools (See https://www.npmjs.com/package/@blockly/dev-tools.)');
useDebugger = false;
}

View File

@@ -295,7 +295,7 @@ export class Drawer {
*/
protected layoutField_(fieldInfo: Icon|Field) {
const svgGroup = Types.isField(fieldInfo) ?
(fieldInfo as Field).field.getSvgRoot() :
(fieldInfo as Field).field.getSvgRoot()! :
(fieldInfo as Icon).icon.iconGroup_!; // Never null in rendered case.
const yPos = fieldInfo.centerline - fieldInfo.height / 2;

View File

@@ -181,7 +181,8 @@ export class MarkerSvg {
// Ensures the marker will be visible immediately after the move.
const animate = this.currentMarkerSvg!.childNodes[0];
if (animate !== undefined) {
animate instanceof SVGAnimationElement && animate.beginElement();
(animate as SVGAnimationElement).beginElement &&
(animate as SVGAnimationElement).beginElement();
}
}

View File

@@ -166,9 +166,9 @@ export class PathObject implements IPathObject {
return;
}
if (add) {
this.svgRoot.classList.add(className);
dom.addClass(this.svgRoot, className);
} else {
this.svgRoot.classList.remove(className);
dom.removeClass(this.svgRoot, className);
}
}

View File

@@ -90,6 +90,11 @@ export class PathObject extends BasePathObject {
override applyColour(block: BlockSvg) {
this.svgPathLight.style.display = '';
this.svgPathDark.style.display = '';
if (!this.style.colourTertiary) {
throw new Error(
'The renderer did not properly initialize the tertiary colour of ' +
'the block style');
}
this.svgPathLight.setAttribute('stroke', this.style.colourTertiary);
this.svgPathDark.setAttribute('fill', this.colourDark);
@@ -118,6 +123,11 @@ export class PathObject extends BasePathObject {
override updateShadow_(shadow: boolean) {
if (shadow) {
this.svgPathLight.style.display = 'none';
if (!this.style.colourSecondary) {
throw new Error(
'The renderer did not properly initialize the secondary colour ' +
'of the block style block style');
}
this.svgPathDark.setAttribute('fill', this.style.colourSecondary);
this.svgPath.setAttribute('stroke', 'none');
this.svgPath.setAttribute('fill', this.style.colourSecondary);

31
core/serialization.ts Normal file
View File

@@ -0,0 +1,31 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Serialization methods.
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.serialization');
import * as blocks from './serialization/blocks.js';
import * as exceptions from './serialization/exceptions.js';
import * as priorities from './serialization/priorities.js';
import * as procedures from './serialization/procedures.js';
import * as registry from './serialization/registry.js';
import * as variables from './serialization/variables.js';
import * as workspaces from './serialization/workspaces.js';
import {ISerializer} from './interfaces/i_serializer.js';
export {
blocks,
exceptions,
priorities,
procedures,
registry,
variables,
workspaces,
ISerializer,
};

View File

@@ -21,6 +21,12 @@ goog.declareModuleId('Blockly.serialization.priorities');
* @alias Blockly.serialization.priorities.VARIABLES
*/
export const VARIABLES = 100;
/**
* The priority for deserializing variable data.
*/
export const PROCEDURES = 75;
/**
* The priority for deserializing blocks.
*

View File

@@ -0,0 +1,152 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {IParameterModel} from '../interfaces/i_parameter_model.js';
import {IProcedureModel} from '../interfaces/i_procedure_model.js';
import type {ISerializer} from '../interfaces/i_serializer.js';
import {ObservableProcedureModel} from '../procedures/observable_procedure_model.js';
import {ObservableParameterModel} from '../procedures/observable_parameter_model.js';
import * as priorities from './priorities.js';
import * as serializationRegistry from './registry.js';
import type {Workspace} from '../workspace.js';
/**
* Representation of a procedure data model.
*/
export interface State {
id: string, name: string, returnTypes: string[]|null,
parameters?: ParameterState[],
}
/**
* Representation of a parameter data model.
*/
export interface ParameterState {
id: string, name: string, types?: string[],
}
/**
* A newable signature for an IProcedureModel.
*
* Refer to
* https://www.typescriptlang.org/docs/handbook/2/generics.html#using-class-types-in-generics
* for what is going on with this.
*/
type ProcedureModelConstructor<ProcedureModel extends IProcedureModel> =
new (workspace: Workspace, name: string, id: string) => ProcedureModel;
/**
* A newable signature for an IParameterModel.
*
* Refer to
* https://www.typescriptlang.org/docs/handbook/2/generics.html#using-class-types-in-generics
* for what is going on with this.
*/
type ParameterModelConstructor<ParameterModel extends IParameterModel> =
new (workspace: Workspace, name: string, id: string) => ParameterModel;
/** Serializes the given IProcedureModel to JSON. */
function saveProcedure(proc: IProcedureModel): State {
const state: State = {
id: proc.getId(),
name: proc.getName(),
returnTypes: proc.getReturnTypes(),
};
if (!proc.getParameters().length) return state;
state.parameters = proc.getParameters().map((param) => saveParameter(param));
return state;
}
/** Serializes the given IParameterModel to JSON. */
function saveParameter(param: IParameterModel): ParameterState {
const state: ParameterState = {
id: param.getId(),
name: param.getName(),
};
if (!param.getTypes().length) return state;
state.types = param.getTypes();
return state;
}
/** Deserializes the given procedure model State from JSON. */
function
loadProcedure<ProcedureModel extends IProcedureModel,
ParameterModel extends IParameterModel>(
procedureModelClass: ProcedureModelConstructor<ProcedureModel>,
parameterModelClass: ParameterModelConstructor<ParameterModel>,
state: State, workspace: Workspace): ProcedureModel {
const proc = new procedureModelClass(workspace, state.name, state.id)
.setReturnTypes(state.returnTypes);
if (!state.parameters) return proc;
for (const [index, param] of state.parameters.entries()) {
proc.insertParameter(
loadParameter(parameterModelClass, param, workspace), index);
}
return proc;
}
/** Deserializes the given ParameterState from JSON. */
function loadParameter<ParameterModel extends IParameterModel>(
parameterModelClass: ParameterModelConstructor<ParameterModel>,
state: ParameterState, workspace: Workspace): ParameterModel {
return new parameterModelClass(workspace, state.name, state.id)
.setTypes(state.types || []);
}
/** Serializer for saving and loading procedure state. */
export class ProcedureSerializer<ProcedureModel extends IProcedureModel,
ParameterModel extends
IParameterModel> implements ISerializer {
public priority = priorities.PROCEDURES;
/**
* Constructs the procedure serializer.
*
* Example usage:
* new ProcedureSerializer(MyProcedureModelClass, MyParameterModelClass)
*
* @param procedureModelClass The class (implementing IProcedureModel) that
* you want this serializer to deserialize.
* @param parameterModelClass The class (implementing IParameterModel) that
* you want this serializer to deserialize.
*/
constructor(
private readonly procedureModelClass:
ProcedureModelConstructor<ProcedureModel>,
private readonly parameterModelClass:
ParameterModelConstructor<ParameterModel>) {}
/** Serializes the procedure models of the given workspace. */
save(workspace: Workspace): State[]|null {
return workspace.getProcedureMap().getProcedures().map(
(proc) => saveProcedure(proc));
}
/**
* Deserializes the procedures models defined by the given state into the
* workspace.
*/
load(state: State[], workspace: Workspace) {
const map = workspace.getProcedureMap();
for (const procState of state) {
map.add(loadProcedure(
this.procedureModelClass, this.parameterModelClass, procState,
workspace));
}
}
/** Disposes of any procedure models that exist on the workspace. */
clear(workspace: Workspace) {
workspace.getProcedureMap().clear();
}
}
serializationRegistry.register(
'procedures',
new ProcedureSerializer(
ObservableProcedureModel, ObservableParameterModel));

View File

@@ -34,7 +34,7 @@ export enum names {
CUT = 'cut',
PASTE = 'paste',
UNDO = 'undo',
REDO = 'redo'
REDO = 'redo',
}
/**

View File

@@ -56,8 +56,7 @@ export class ShortcutRegistry {
register(shortcut: KeyboardShortcut, opt_allowOverrides?: boolean) {
const registeredShortcut = this.shortcuts.get(shortcut.name);
if (registeredShortcut && !opt_allowOverrides) {
throw new Error(
'Shortcut with name "' + shortcut.name + '" already exists.');
throw new Error(`Shortcut named "${shortcut.name}" already exists.`);
}
this.shortcuts.set(shortcut.name, shortcut);
@@ -81,8 +80,7 @@ export class ShortcutRegistry {
const shortcut = this.shortcuts.get(shortcutName);
if (!shortcut) {
console.warn(
'Keyboard shortcut with name "' + shortcutName + '" not found.');
console.warn(`Keyboard shortcut named "${shortcutName}" not found.`);
return false;
}
@@ -110,9 +108,8 @@ export class ShortcutRegistry {
keyCode = String(keyCode);
const shortcutNames = this.keyMap.get(keyCode);
if (shortcutNames && !opt_allowCollision) {
throw new Error(
'Shortcut with name "' + shortcutName + '" collides with shortcuts ' +
shortcutNames.toString());
throw new Error(`Shortcut named "${
shortcutName}" collides with shortcuts "${shortcutNames}"`);
} else if (shortcutNames && opt_allowCollision) {
shortcutNames.unshift(shortcutName);
} else {
@@ -138,9 +135,8 @@ export class ShortcutRegistry {
if (!shortcutNames) {
if (!opt_quiet) {
console.warn(
'No keyboard shortcut with name "' + shortcutName +
'" registered with key code "' + keyCode + '"');
console.warn(`No keyboard shortcut named "${
shortcutName}" registered with key code "${keyCode}"`);
}
return false;
}
@@ -154,9 +150,8 @@ export class ShortcutRegistry {
return true;
}
if (!opt_quiet) {
console.warn(
'No keyboard shortcut with name "' + shortcutName +
'" registered with key code "' + keyCode + '"');
console.warn(`No keyboard shortcut named "${
shortcutName}" registered with key code "${keyCode}"`);
}
return false;
}

Some files were not shown because too many files have changed in this diff Show More