release: Merge branch 'develop' into rc/v9.2.0

This commit is contained in:
Christopher Allen
2022-12-15 17:59:59 +00:00
183 changed files with 11508 additions and 7021 deletions

View File

@@ -7,7 +7,7 @@
/tests/compile/*
/tests/jsunit/*
/tests/generators/*
/tests/mocha/run_mocha_tests_in_browser.js
/tests/mocha/webdriver.js
/tests/screenshot/*
/tests/test_runner.js
/tests/workspace_svg/*

View File

@@ -10,12 +10,6 @@ updates:
target-branch: "develop"
schedule:
interval: "weekly"
ignore:
- dependency-name: "jsdom"
# For jsdom, ignore all updates for version 16.
# We should test that this does not cause issue
# google/blockly-samples#665 when version 17 is released.
versions: "16.x"
commit-message:
prefix: "chore(deps)"
labels:

16
.github/release.yml vendored
View File

@@ -4,23 +4,29 @@ changelog:
exclude:
labels:
- ignore-for-release
- "PR: chore"
authors:
- dependabot
categories:
- title: Breaking Changes 🛠
- title: Breaking changes 🛠
labels:
- breaking change
- title: New Features
- title: Deprecations 🧹 - APIs that may be removed in future releases
labels:
- deprecation
- title: New features ✨
labels:
- "PR: feature"
- title: Bug fixes
- title: Bug fixes 🐛
labels:
- "PR: fix"
- title: Cleanup ♻️
labels:
- "PR: chore"
- "PR: docs"
- "PR: refactor"
- title: Other Changes
- title: Reverted changes
labels:
- "PR: revert"
- title: Other changes
labels:
- "*"

View File

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

View File

@@ -13,14 +13,14 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: DoozyX/clang-format-lint-action@v0.14
- uses: DoozyX/clang-format-lint-action@v0.15
with:
source: 'core'
extensions: 'js,ts'
# This should be as close as possible to the version that the npm
# package supports. This can be found by running:
# npx clang-format --version.
clangFormatVersion: 13
clangFormatVersion: 15
# The Report clang format workflow (report_clang_format.yml) will
# run (if required) after this one to post a comment to the PR.

View File

@@ -1,6 +1,8 @@
on:
pull_request_target:
types: [ opened, edited ]
types:
- opened
- edited
name: conventional-release-labels
jobs:
label:
@@ -8,5 +10,7 @@ jobs:
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"}'
type_labels: '{"feat": "PR: feature", "fix": "PR: fix", "breaking": "breaking
change", "chore": "PR: chore", "docs": "PR: docs", "refactor": "PR:
refactor", "revert": "PR: revert", "deprecate": "deprecation"}'
ignored_types: '[]'

View File

@@ -36,7 +36,7 @@ jobs:
run: source ./tests/scripts/update_metadata.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@b4d51739f96fca8047ad065eccef63442d8e99f7
uses: peter-evans/create-pull-request@2b011faafdcbc9ceb11414d64d0573f37c774b04
with:
commit-message: Update build artifact sizes in check_metadata.sh
delete-branch: true

View File

@@ -0,0 +1,11 @@
// Added November 2022 after discovering that a number of orgs were hot-linking
// their Blockly applications to https://blockly-demo.appspot.com/
// Delete this file in early 2024.
var msg = 'Compiled Blockly files should be loaded from https://unpkg.com/blockly/\n' +
'For help, contact https://groups.google.com/g/blockly';
console.log(msg);
try {
alert(msg);
} catch (_e) {
// Can't alert? Probably node.js.
}

View File

@@ -979,7 +979,7 @@ const PROCEDURE_CALL_COMMON = {
Xml.domToWorkspace(xml, this.workspace);
Events.setGroup(false);
}
} else if (event.type === Events.BLOCK_DELETE && event.blockId != this.id) {
} else if (event.type === Events.BLOCK_DELETE) {
// Look for the case where a procedure definition has been deleted,
// leaving this block (a procedure call) orphaned. In this case, delete
// the orphan.

View File

@@ -95,6 +95,9 @@ export class Block implements IASTNodeLocation, IDeletable {
/** An optional method called during initialization. */
init?: (() => AnyDuringMigration)|null = undefined;
/** An optional method called during disposal. */
destroy?: (() => void) = undefined;
/**
* An optional serialization method for defining how to serialize the
* mutation state to XML. This must be coupled with defining
@@ -324,16 +327,15 @@ export class Block implements IASTNodeLocation, IDeletable {
if (this.isDeadOrDying()) {
return;
}
// Terminate onchange event calls.
if (this.onchangeWrapper_) {
this.workspace.removeChangeListener(this.onchangeWrapper_);
}
this.unplug(healStack);
if (eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_DELETE))(this));
}
if (this.onchangeWrapper_) {
this.workspace.removeChangeListener(this.onchangeWrapper_);
}
eventUtils.disable();
try {
@@ -362,6 +364,9 @@ export class Block implements IASTNodeLocation, IDeletable {
}
} finally {
eventUtils.enable();
if (typeof this.destroy === 'function') {
this.destroy();
}
this.disposed = true;
}
}
@@ -1817,7 +1822,7 @@ export class Block implements IASTNodeLocation, IDeletable {
* @param element The element to try to turn into a field.
* @returns The field defined by the JSON, or null if one couldn't be created.
*/
private fieldFromJson_(element: {alt?: string, type?: string, text?: string}):
private fieldFromJson_(element: {alt?: string, type: string, text?: string}):
Field|null {
const field = fieldRegistry.fromJson(element);
if (!field && element['alt']) {

View File

@@ -176,7 +176,7 @@ export class BlockDragger implements IBlockDragger {
* @param currentDragDeltaXY How far the pointer has moved from the position
* at the start of the drag, in pixel units.
*/
drag(e: Event, currentDragDeltaXY: Coordinate) {
drag(e: PointerEvent, currentDragDeltaXY: Coordinate) {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.moveDuringDrag(newLoc);
@@ -187,7 +187,7 @@ export class BlockDragger implements IBlockDragger {
this.draggedConnectionManager_.update(delta, this.dragTarget_);
const oldWouldDeleteBlock = this.wouldDeleteBlock_;
this.wouldDeleteBlock_ = this.draggedConnectionManager_.wouldDeleteBlock();
this.wouldDeleteBlock_ = this.draggedConnectionManager_.wouldDeleteBlock;
if (oldWouldDeleteBlock !== this.wouldDeleteBlock_) {
// Prevent unnecessary add/remove class calls.
this.updateCursorDuringBlockDrag_();
@@ -205,11 +205,11 @@ export class BlockDragger implements IBlockDragger {
/**
* Finish a block drag and put the block back on the workspace.
*
* @param e The mouseup/touchend event.
* @param e The pointerup event.
* @param currentDragDeltaXY How far the pointer has moved from the position
* at the start of the drag, in pixel units.
*/
endDrag(e: Event, currentDragDeltaXY: Coordinate) {
endDrag(e: PointerEvent, currentDragDeltaXY: Coordinate) {
// Make sure internal state is fresh.
this.drag(e, currentDragDeltaXY);
this.dragIconData_ = [];

View File

@@ -227,7 +227,8 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
this.pathObject.updateMovable(this.isMovable());
const svg = this.getSvgRoot();
if (!this.workspace.options.readOnly && !this.eventsInit_ && svg) {
browserEvents.conditionalBind(svg, 'mousedown', this, this.onMouseDown_);
browserEvents.conditionalBind(
svg, 'pointerdown', this, this.onMouseDown_);
}
this.eventsInit_ = true;
@@ -673,11 +674,11 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
}
/**
* Handle a mouse-down on an SVG block.
* Handle a pointerdown on an SVG block.
*
* @param e Mouse down event or touch start event.
* @param e Pointer down event.
*/
private onMouseDown_(e: Event) {
private onMouseDown_(e: PointerEvent) {
const gesture = this.workspace.getGesture(e);
if (gesture) {
gesture.handleBlockStart(e, this);

View File

@@ -53,19 +53,19 @@ import {DragTarget} from './drag_target.js';
import * as dropDownDiv from './dropdowndiv.js';
import * as Events from './events/events.js';
import * as Extensions from './extensions.js';
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, MenuGenerator, MenuGeneratorFunction, MenuOption} from './field_dropdown.js';
import {Field, FieldValidator} from './field.js';
import {FieldAngle, FieldAngleValidator} from './field_angle.js';
import {FieldCheckbox, FieldCheckboxValidator} from './field_checkbox.js';
import {FieldColour, FieldColourValidator} from './field_colour.js';
import {FieldDropdown, FieldDropdownValidator, 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';
import {FieldMultilineInput} from './field_multilineinput.js';
import {FieldNumber} from './field_number.js';
import {FieldMultilineInput, FieldMultilineInputValidator} from './field_multilineinput.js';
import {FieldNumber, FieldNumberValidator} from './field_number.js';
import * as fieldRegistry from './field_registry.js';
import {FieldTextInput} from './field_textinput.js';
import {FieldVariable} from './field_variable.js';
import {FieldTextInput, FieldTextInputValidator} from './field_textinput.js';
import {FieldVariable, FieldVariableValidator} from './field_variable.js';
import {Flyout} from './flyout_base.js';
import {FlyoutButton} from './flyout_button.js';
import {HorizontalFlyout} from './flyout_horizontal.js';
@@ -101,7 +101,6 @@ import {IMetricsManager} from './interfaces/i_metrics_manager.js';
import {IMovable} from './interfaces/i_movable.js';
import {IPositionable} from './interfaces/i_positionable.js';
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 {IStyleable} from './interfaces/i_styleable.js';
@@ -146,7 +145,6 @@ import {Toolbox} from './toolbox/toolbox.js';
import {ToolboxItem} from './toolbox/toolbox_item.js';
import * as Tooltip from './tooltip.js';
import * as Touch from './touch.js';
import {TouchGesture} from './touch_gesture.js';
import {Trashcan} from './trashcan.js';
import * as utils from './utils.js';
import * as colour from './utils/colour.js';
@@ -486,9 +484,7 @@ export function unbindEvent_(bindData: browserEvents.Data): Function {
* @param opt_noCaptureIdentifier True if triggering on this event should not
* block execution of other event handlers on this touch or other
* simultaneous touches. False by default.
* @param opt_noPreventDefault True if triggering on this event should prevent
* the default handler. False by default. If opt_noPreventDefault is
* provided, opt_noCaptureIdentifier must also be provided.
* @param _opt_noPreventDefault No-op, deprecated and will be removed in v10.
* @returns Opaque data that can be passed to unbindEvent_.
* @deprecated Use **Blockly.browserEvents.conditionalBind** instead.
* @see browserEvents.conditionalBind
@@ -497,13 +493,12 @@ export function unbindEvent_(bindData: browserEvents.Data): Function {
export function bindEventWithChecks_(
node: EventTarget, name: string, thisObject: Object|null, func: Function,
opt_noCaptureIdentifier?: boolean,
opt_noPreventDefault?: boolean): browserEvents.Data {
_opt_noPreventDefault?: boolean): browserEvents.Data {
deprecation.warn(
'Blockly.bindEventWithChecks_', 'December 2021', 'December 2022',
'Blockly.browserEvents.conditionalBind');
return browserEvents.conditionalBind(
node, name, thisObject, func, opt_noCaptureIdentifier,
opt_noPreventDefault);
node, name, thisObject, func, opt_noCaptureIdentifier);
}
// Aliases to allow external code to access these values for legacy reasons.
@@ -648,24 +643,31 @@ export {Cursor};
export {DeleteArea};
export {DragTarget};
export const DropDownDiv = dropDownDiv;
export {Field};
export {FieldAngle};
export {FieldCheckbox};
export {FieldColour};
export {FieldDropdown, MenuGenerator, MenuGeneratorFunction, MenuOption};
export {Field, FieldValidator};
export {FieldAngle, FieldAngleValidator};
export {FieldCheckbox, FieldCheckboxValidator};
export {FieldColour, FieldColourValidator};
export {
FieldDropdown,
FieldDropdownValidator,
MenuGenerator,
MenuGeneratorFunction,
MenuOption,
};
export {FieldImage};
export {FieldLabel};
export {FieldLabelSerializable};
export {FieldMultilineInput};
export {FieldNumber};
export {FieldTextInput};
export {FieldVariable};
export {FieldMultilineInput, FieldMultilineInputValidator};
export {FieldNumber, FieldNumberValidator};
export {FieldTextInput, FieldTextInputValidator};
export {FieldVariable, FieldVariableValidator};
export {Flyout};
export {FlyoutButton};
export {FlyoutMetricsManager};
export {CodeGenerator};
export {CodeGenerator as Generator}; // Deprecated name, October 2022.
export {Gesture};
export {Gesture as TouchGesture}; // Remove in v10.
export {Grid};
export {HorizontalFlyout};
export {IASTNodeLocation};
@@ -693,7 +695,6 @@ export {Input};
export {InsertionMarkerManager};
export {IPositionable};
export {IRegistrable};
export {IRegistrableField};
export {ISelectable};
export {ISelectableToolboxItem};
export {IStyleable};
@@ -719,7 +720,6 @@ export {Toolbox};
export {ToolboxCategory};
export {ToolboxItem};
export {ToolboxSeparator};
export {TouchGesture};
export {Trashcan};
export {VariableMap};
export {VariableModel};

View File

@@ -32,6 +32,7 @@ export interface BlocklyOptions {
maxBlocks?: number;
maxInstances?: {[blockType: string]: number};
media?: string;
modalInputs?: boolean;
move?: MoveOptions;
oneBasedIndex?: boolean;
readOnly?: boolean;

View File

@@ -14,7 +14,7 @@ goog.declareModuleId('Blockly.blocks');
/**
* A block definition. For now this very lose, but it can potentially
* A block definition. For now this very loose, but it can potentially
* be refined e.g. by replacing this typedef with a class definition.
*/
export type BlockDefinition = AnyDuringMigration;

View File

@@ -13,6 +13,7 @@ import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.browserEvents');
import * as Touch from './touch.js';
import * as deprecation from './utils/deprecation.js';
import * as userAgent from './utils/useragent.js';
@@ -51,42 +52,36 @@ const PAGE_MODE_MULTIPLIER = 125;
* @param opt_noCaptureIdentifier True if triggering on this event should not
* block execution of other event handlers on this touch or other
* simultaneous touches. False by default.
* @param opt_noPreventDefault True if triggering on this event should prevent
* the default handler. False by default. If opt_noPreventDefault is
* provided, opt_noCaptureIdentifier must also be provided.
* @param opt_noPreventDefault No-op, deprecated and will be removed in v10.
* @returns Opaque data that can be passed to unbindEvent_.
* @alias Blockly.browserEvents.conditionalBind
*/
export function conditionalBind(
node: EventTarget, name: string, thisObject: Object|null, func: Function,
opt_noCaptureIdentifier?: boolean, opt_noPreventDefault?: boolean): Data {
let handled = false;
if (opt_noPreventDefault !== undefined) {
deprecation.warn(
'The opt_noPreventDefault argument of conditionalBind', 'version 9',
'version 10');
}
/**
*
* @param e
*/
function wrapFunc(e: Event) {
const captureIdentifier = !opt_noCaptureIdentifier;
// Handle each touch point separately. If the event was a mouse event, this
// will hand back an array with one element, which we're fine handling.
const events = Touch.splitEventByTouches(e);
for (let i = 0; i < events.length; i++) {
const event = events[i];
if (captureIdentifier && !Touch.shouldHandleEvent(event)) {
continue;
}
Touch.setClientFromTouch(event);
if (!(captureIdentifier && !Touch.shouldHandleEvent(e))) {
if (thisObject) {
func.call(thisObject, event);
func.call(thisObject, e);
} else {
func(event);
func(e);
}
handled = true;
}
}
const bindData: Data = [];
if (globalThis['PointerEvent'] && name in Touch.TOUCH_MAP) {
if (name in Touch.TOUCH_MAP) {
for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) {
const type = Touch.TOUCH_MAP[name][i];
node.addEventListener(type, wrapFunc, false);
@@ -95,24 +90,6 @@ export function conditionalBind(
} else {
node.addEventListener(name, wrapFunc, false);
bindData.push([node, name, wrapFunc]);
// Add equivalent touch event.
if (name in Touch.TOUCH_MAP) {
const touchWrapFunc = (e: Event) => {
wrapFunc(e);
// Calling preventDefault stops the browser from scrolling/zooming the
// page.
const preventDef = !opt_noPreventDefault;
if (handled && preventDef) {
e.preventDefault();
}
};
for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) {
const type = Touch.TOUCH_MAP[name][i];
node.addEventListener(type, touchWrapFunc, false);
bindData.push([node, type, touchWrapFunc]);
}
}
}
return bindData;
}
@@ -146,7 +123,7 @@ export function bind(
}
const bindData: Data = [];
if (globalThis['PointerEvent'] && name in Touch.TOUCH_MAP) {
if (name in Touch.TOUCH_MAP) {
for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) {
const type = Touch.TOUCH_MAP[name][i];
node.addEventListener(type, wrapFunc, false);
@@ -155,32 +132,6 @@ export function bind(
} else {
node.addEventListener(name, wrapFunc, false);
bindData.push([node, name, wrapFunc]);
// Add equivalent touch event.
if (name in Touch.TOUCH_MAP) {
const touchWrapFunc = (e: Event) => {
// Punt on multitouch events.
if (e instanceof TouchEvent && e.changedTouches &&
e.changedTouches.length === 1) {
// Map the touch event's properties to the event.
const touchPoint = e.changedTouches[0];
// TODO (6311): We are trying to make a touch event look like a mouse
// event, which is not allowed, because it requires adding more
// properties to the event. How do we want to deal with this?
(e as AnyDuringMigration).clientX = touchPoint.clientX;
(e as AnyDuringMigration).clientY = touchPoint.clientY;
}
wrapFunc(e);
// Stop the browser from scrolling/zooming the page.
e.preventDefault();
};
for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) {
const type = Touch.TOUCH_MAP[name][i];
node.addEventListener(type, touchWrapFunc, false);
bindData.push([node, type, touchWrapFunc]);
}
}
}
return bindData;
}

View File

@@ -249,10 +249,10 @@ export class Bubble implements IBubble {
if (!this.workspace_.options.readOnly) {
this.onMouseDownBubbleWrapper = browserEvents.conditionalBind(
this.bubbleBack, 'mousedown', this, this.bubbleMouseDown);
this.bubbleBack, 'pointerdown', this, this.bubbleMouseDown);
if (this.resizeGroup) {
this.onMouseDownResizeWrapper = browserEvents.conditionalBind(
this.resizeGroup, 'mousedown', this, this.resizeMouseDown);
this.resizeGroup, 'pointerdown', this, this.resizeMouseDown);
}
}
this.bubbleGroup.appendChild(content);
@@ -278,11 +278,11 @@ export class Bubble implements IBubble {
}
/**
* Handle a mouse-down on bubble's border.
* Handle a pointerdown on bubble's border.
*
* @param e Mouse down event.
* @param e Pointer down event.
*/
private bubbleMouseDown(e: Event) {
private bubbleMouseDown(e: PointerEvent) {
const gesture = this.workspace_.getGesture(e);
if (gesture) {
gesture.handleBubbleStart(e, this);
@@ -318,11 +318,11 @@ export class Bubble implements IBubble {
// NOP if bubble is not deletable.
/**
* Handle a mouse-down on bubble's resize corner.
* Handle a pointerdown on bubble's resize corner.
*
* @param e Mouse down event.
* @param e Pointer down event.
*/
private resizeMouseDown(e: MouseEvent) {
private resizeMouseDown(e: PointerEvent) {
this.promote();
Bubble.unbindDragEvents();
if (browserEvents.isRightButton(e)) {
@@ -337,20 +337,20 @@ export class Bubble implements IBubble {
this.workspace_.RTL ? -this.width : this.width, this.height));
Bubble.onMouseUpWrapper = browserEvents.conditionalBind(
document, 'mouseup', this, Bubble.bubbleMouseUp);
document, 'pointerup', this, Bubble.bubbleMouseUp);
Bubble.onMouseMoveWrapper = browserEvents.conditionalBind(
document, 'mousemove', this, this.resizeMouseMove);
document, 'pointermove', this, this.resizeMouseMove);
this.workspace_.hideChaff();
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
}
/**
* Resize this bubble to follow the mouse.
* Resize this bubble to follow the pointer.
*
* @param e Mouse move event.
* @param e Pointer move event.
*/
private resizeMouseMove(e: MouseEvent) {
private resizeMouseMove(e: PointerEvent) {
this.autoLayout = false;
const newXY = this.workspace_.moveDrag(e);
this.setBubbleSize(this.workspace_.RTL ? -newXY.x : newXY.x, newXY.y);
@@ -847,11 +847,11 @@ export class Bubble implements IBubble {
}
/**
* Handle a mouse-up event while dragging a bubble's border or resize handle.
* Handle a pointerup event while dragging a bubble's border or resize handle.
*
* @param _e Mouse up event.
* @param _e Pointer up event.
*/
private static bubbleMouseUp(_e: MouseEvent) {
private static bubbleMouseUp(_e: PointerEvent) {
Touch.clearTouchIdentifier();
Bubble.unbindDragEvents();
}

View File

@@ -89,7 +89,7 @@ export class BubbleDragger {
* at the start of the drag, in pixel units.
* @internal
*/
dragBubble(e: Event, currentDragDeltaXY: Coordinate) {
dragBubble(e: PointerEvent, currentDragDeltaXY: Coordinate) {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.bubble.moveDuringDrag(this.dragSurface_, newLoc);
@@ -141,12 +141,12 @@ export class BubbleDragger {
/**
* Finish a bubble drag and put the bubble back on the workspace.
*
* @param e The mouseup/touchend event.
* @param e The pointerup event.
* @param currentDragDeltaXY How far the pointer has moved from the position
* at the start of the drag, in pixel units.
* @internal
*/
endBubbleDrag(e: Event, currentDragDeltaXY: Coordinate) {
endBubbleDrag(e: PointerEvent, currentDragDeltaXY: Coordinate) {
// Make sure internal state is fresh.
this.dragBubble(e, currentDragDeltaXY);

View File

@@ -149,11 +149,8 @@ export class Comment extends Icon {
body.appendChild(textarea);
this.foreignObject!.appendChild(body);
// Ideally this would be hooked to the focus event for the comment.
// However doing so in Firefox swallows the cursor for unknown reasons.
// So this is hooked to mouseup instead. No big deal.
this.onMouseUpWrapper = browserEvents.conditionalBind(
textarea, 'mouseup', this, this.startEdit, true, true);
textarea, 'focus', this, this.startEdit, true);
// Don't zoom with mousewheel.
this.onWheelWrapper = browserEvents.conditionalBind(
textarea, 'wheel', this, function(e: Event) {
@@ -315,7 +312,7 @@ export class Comment extends Icon {
*
* @param _e Mouse up event.
*/
private startEdit(_e: Event) {
private startEdit(_e: PointerEvent) {
if (this.bubble_?.promote()) {
// Since the act of moving this node within the DOM causes a loss of
// focus, we need to reapply the focus.

View File

@@ -156,6 +156,13 @@ export function setBoundsElement(boundsElem: Element|null) {
boundsElement = boundsElem;
}
/**
* @returns The field that currently owns this, or null.
*/
export function getOwner(): Field|null {
return owner;
}
/**
* Provide the div for inserting content into the drop-down.
*
@@ -194,11 +201,12 @@ export function setColour(backgroundColour: string, borderColour: string) {
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
* @returns True if the menu rendered below block; false if above.
*/
export function showPositionedByBlock(
field: Field, block: BlockSvg, opt_onHide?: Function,
export function showPositionedByBlock<T>(
field: Field<T>, block: BlockSvg, opt_onHide?: Function,
opt_secondaryYOffset?: number): boolean {
return showPositionedByRect(
getScaledBboxOfBlock(block), field, opt_onHide, opt_secondaryYOffset);
getScaledBboxOfBlock(block), field as Field, opt_onHide,
opt_secondaryYOffset);
}
/**
@@ -212,12 +220,13 @@ export function showPositionedByBlock(
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
* @returns True if the menu rendered below block; false if above.
*/
export function showPositionedByField(
field: Field, opt_onHide?: Function,
export function showPositionedByField<T>(
field: Field<T>, opt_onHide?: Function,
opt_secondaryYOffset?: number): boolean {
positionToField = true;
return showPositionedByRect(
getScaledBboxOfField(field), field, opt_onHide, opt_secondaryYOffset);
getScaledBboxOfField(field as Field), field as Field, opt_onHide,
opt_secondaryYOffset);
}
/**
* Get the scaled bounding box of a block.
@@ -300,10 +309,10 @@ function showPositionedByRect(
* @returns True if the menu rendered at the primary origin point.
* @internal
*/
export function show(
newOwner: Field, rtl: boolean, primaryX: number, primaryY: number,
export function show<T>(
newOwner: Field<T>, rtl: boolean, primaryX: number, primaryY: number,
secondaryX: number, secondaryY: number, opt_onHide?: Function): boolean {
owner = newOwner;
owner = newOwner as Field;
onHide = opt_onHide || null;
// Set direction.
div.style.direction = rtl ? 'rtl' : 'ltr';
@@ -540,8 +549,8 @@ export function isVisible(): boolean {
* animating.
* @returns True if hidden.
*/
export function hideIfOwner(
divOwner: Field, opt_withoutAnimation?: boolean): boolean {
export function hideIfOwner<T>(
divOwner: Field<T>, opt_withoutAnimation?: boolean): boolean {
if (owner === divOwner) {
if (opt_withoutAnimation) {
hideWithoutAnimation();

View File

@@ -28,6 +28,16 @@ import {CommentCreate, CommentCreateJson} from './events_comment_create.js';
import {CommentDelete} from './events_comment_delete.js';
import {CommentMove, CommentMoveJson} from './events_comment_move.js';
import {MarkerMove, MarkerMoveJson} from './events_marker_move.js';
import {ProcedureBase} from './events_procedure_base.js';
import {ProcedureChangeReturn} from './events_procedure_change_return.js';
import {ProcedureCreate} from './events_procedure_create.js';
import {ProcedureDelete} from './events_procedure_delete.js';
import {ProcedureEnable} from './events_procedure_enable.js';
import {ProcedureRename} from './events_procedure_rename.js';
import {ProcedureParameterBase} from './events_procedure_parameter_base.js';
import {ProcedureParameterCreate} from './events_procedure_parameter_create.js';
import {ProcedureParameterDelete} from './events_procedure_parameter_delete.js';
import {ProcedureParameterRename} from './events_procedure_parameter_rename.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';
@@ -77,6 +87,16 @@ export {FinishedLoading};
export {FinishedLoadingJson};
export {MarkerMove};
export {MarkerMoveJson};
export {ProcedureBase};
export {ProcedureChangeReturn};
export {ProcedureCreate};
export {ProcedureDelete};
export {ProcedureEnable};
export {ProcedureRename};
export {ProcedureParameterBase};
export {ProcedureParameterCreate};
export {ProcedureParameterDelete};
export {ProcedureParameterRename};
export {Selected};
export {SelectedJson};
export {ThemeChange};

View File

@@ -13,6 +13,7 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.Abstract');
import * as deprecation from '../utils/deprecation.js';
import * as common from '../common.js';
import type {Workspace} from '../workspace.js';
@@ -25,7 +26,10 @@ import * as eventUtils from './utils.js';
* @alias Blockly.Events.Abstract
*/
export abstract class Abstract {
/** Whether or not the event is blank (to be populated by fromJson). */
/**
* Whether or not the event was constructed without necessary parameters
* (to be populated by fromJson).
*/
abstract isBlank: boolean;
/** The workspace identifier for this event. */
@@ -74,6 +78,26 @@ export abstract class Abstract {
this.group = json['group'] || '';
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of Abstract (like all events), but we can't specify that due to the
* fact that parameters to static methods in subclasses must be
* supertypes of parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: AbstractEventJson, workspace: Workspace, event: any):
Abstract {
deprecation.warn(
'Blockly.Events.Abstract.prototype.fromJson', 'version 9', 'version 10',
'Blockly.Events.fromJson');
event.isBlank = false;
event.group = json['group'] || '';
event.workspaceId = workspace.id;
return event;
}
/**
* Does this event record any change of state?
*
@@ -88,8 +112,10 @@ export abstract class Abstract {
*
* @param _forward True if run forward, false if run backward (undo).
*/
run(_forward: boolean) {}
// Defined by subclasses.
run(_forward: boolean) {
// Defined by subclasses. Cannot be abstract b/c UI events do /not/ define
// this.
}
/**
* Get workspace the event belongs to.

View File

@@ -13,6 +13,8 @@ import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockBase');
import type {Block} from '../block.js';
import * as deprecation from '../utils/deprecation.js';
import type {Workspace} from '../workspace.js';
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js';
@@ -32,7 +34,7 @@ export class BlockBase extends AbstractEvent {
*/
constructor(opt_block?: Block) {
super();
this.isBlank = !!opt_block;
this.isBlank = !opt_block;
if (!opt_block) return;
@@ -48,7 +50,7 @@ export class BlockBase extends AbstractEvent {
*
* @returns JSON representation.
*/
override toJson(): AbstractEventJson {
override toJson(): BlockBaseJson {
const json = super.toJson() as BlockBaseJson;
if (!this.blockId) {
throw new Error(
@@ -65,9 +67,29 @@ export class BlockBase extends AbstractEvent {
* @param json JSON representation.
*/
override fromJson(json: BlockBaseJson) {
deprecation.warn(
'Blockly.Events.BlockBase.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.blockId = json['blockId'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of BlockBase, but we can't specify that due to the fact that parameters
* to static methods in subclasses must be supertypes of parameters to
* static methods in superclasses.
* @internal
*/
static fromJson(json: BlockBaseJson, workspace: Workspace, event?: any):
BlockBase {
const newEvent =
super.fromJson(json, workspace, event ?? new BlockBase()) as BlockBase;
newEvent.blockId = json['blockId'];
return newEvent;
}
}
export interface BlockBaseJson extends AbstractEventJson {

View File

@@ -14,7 +14,9 @@ goog.declareModuleId('Blockly.Events.BlockChange');
import type {Block} from '../block.js';
import type {BlockSvg} from '../block_svg.js';
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import {Workspace} from '../workspace.js';
import * as Xml from '../xml.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';
@@ -79,6 +81,9 @@ export class BlockChange extends BlockBase {
* @param json JSON representation.
*/
override fromJson(json: BlockChangeJson) {
deprecation.warn(
'Blockly.Events.BlockChange.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.element = json['element'];
this.name = json['name'];
@@ -86,6 +91,27 @@ export class BlockChange extends BlockBase {
this.newValue = json['newValue'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of BlockChange, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: BlockChangeJson, workspace: Workspace, event?: any):
BlockChange {
const newEvent =
super.fromJson(json, workspace, event ?? new BlockChange()) as
BlockChange;
newEvent.element = json['element'];
newEvent.name = json['name'];
newEvent.oldValue = json['oldValue'];
newEvent.newValue = json['newValue'];
return newEvent;
}
/**
* Does this event record any change of state?
*

View File

@@ -13,12 +13,14 @@ import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockCreate');
import type {Block} from '../block.js';
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import * as blocks from '../serialization/blocks.js';
import * as Xml from '../xml.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';
import * as eventUtils from './utils.js';
import {Workspace} from '../workspace.js';
/**
@@ -89,6 +91,9 @@ export class BlockCreate extends BlockBase {
* @param json JSON representation.
*/
override fromJson(json: BlockCreateJson) {
deprecation.warn(
'Blockly.Events.BlockCreate.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.xml = Xml.textToDom(json['xml']);
this.ids = json['ids'];
@@ -98,6 +103,29 @@ export class BlockCreate extends BlockBase {
}
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of BlockCreate, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: BlockCreateJson, workspace: Workspace, event?: any):
BlockCreate {
const newEvent =
super.fromJson(json, workspace, event ?? new BlockCreate()) as
BlockCreate;
newEvent.xml = Xml.textToDom(json['xml']);
newEvent.ids = json['ids'];
newEvent.json = json['json'] as blocks.State;
if (json['recordUndo'] !== undefined) {
newEvent.recordUndo = json['recordUndo'];
}
return newEvent;
}
/**
* Run a creation event.
*

View File

@@ -13,12 +13,14 @@ import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockDelete');
import type {Block} from '../block.js';
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import * as blocks from '../serialization/blocks.js';
import * as Xml from '../xml.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';
import * as eventUtils from './utils.js';
import {Workspace} from '../workspace.js';
/**
@@ -103,6 +105,9 @@ export class BlockDelete extends BlockBase {
* @param json JSON representation.
*/
override fromJson(json: BlockDeleteJson) {
deprecation.warn(
'Blockly.Events.BlockDelete.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.oldXml = Xml.textToDom(json['oldXml']);
this.ids = json['ids'];
@@ -114,6 +119,31 @@ export class BlockDelete extends BlockBase {
}
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of BlockDelete, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: BlockDeleteJson, workspace: Workspace, event?: any):
BlockDelete {
const newEvent =
super.fromJson(json, workspace, event ?? new BlockDelete()) as
BlockDelete;
newEvent.oldXml = Xml.textToDom(json['oldXml']);
newEvent.ids = json['ids'];
newEvent.wasShadow =
json['wasShadow'] || newEvent.oldXml.tagName.toLowerCase() === 'shadow';
newEvent.oldJson = json['oldJson'];
if (json['recordUndo'] !== undefined) {
newEvent.recordUndo = json['recordUndo'];
}
return newEvent;
}
/**
* Run a deletion event.
*

View File

@@ -13,10 +13,12 @@ import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockDrag');
import type {Block} from '../block.js';
import * as deprecation from '../utils/deprecation.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';
import {Workspace} from '../workspace.js';
/**
@@ -83,11 +85,33 @@ export class BlockDrag extends UiBase {
* @param json JSON representation.
*/
override fromJson(json: BlockDragJson) {
deprecation.warn(
'Blockly.Events.BlockDrag.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.isStart = json['isStart'];
this.blockId = json['blockId'];
this.blocks = json['blocks'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of BlockDrag, but we can't specify that due to the fact that parameters
* to static methods in subclasses must be supertypes of parameters to
* static methods in superclasses..
* @internal
*/
static fromJson(json: BlockDragJson, workspace: Workspace, event?: any):
BlockDrag {
const newEvent =
super.fromJson(json, workspace, event ?? new BlockDrag()) as BlockDrag;
newEvent.isStart = json['isStart'];
newEvent.blockId = json['blockId'];
newEvent.blocks = json['blocks'];
return newEvent;
}
}
export interface BlockDragJson extends AbstractEventJson {

View File

@@ -14,11 +14,13 @@ goog.declareModuleId('Blockly.Events.BlockMove');
import type {Block} from '../block.js';
import {ConnectionType} from '../connection_type.js';
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import {Coordinate} from '../utils/coordinate.js';
import {BlockBase, BlockBaseJson} from './events_block_base.js';
import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js';
interface BlockLocation {
@@ -68,6 +70,12 @@ export class BlockMove extends BlockBase {
*/
override toJson(): BlockMoveJson {
const json = super.toJson() as BlockMoveJson;
json['oldParentId'] = this.oldParentId;
json['oldInputName'] = this.oldInputName;
if (this.oldCoordinate) {
json['oldCoordinate'] = `${Math.round(this.oldCoordinate.x)}, ` +
`${Math.round(this.oldCoordinate.y)}`;
}
json['newParentId'] = this.newParentId;
json['newInputName'] = this.newInputName;
if (this.newCoordinate) {
@@ -86,7 +94,16 @@ export class BlockMove extends BlockBase {
* @param json JSON representation.
*/
override fromJson(json: BlockMoveJson) {
deprecation.warn(
'Blockly.Events.BlockMove.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.oldParentId = json['oldParentId'];
this.oldInputName = json['oldInputName'];
if (json['oldCoordinate']) {
const xy = json['oldCoordinate'].split(',');
this.oldCoordinate = new Coordinate(Number(xy[0]), Number(xy[1]));
}
this.newParentId = json['newParentId'];
this.newInputName = json['newInputName'];
if (json['newCoordinate']) {
@@ -98,6 +115,37 @@ export class BlockMove extends BlockBase {
}
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of BlockMove, but we can't specify that due to the fact that parameters
* to static methods in subclasses must be supertypes of parameters to
* static methods in superclasses.
* @internal
*/
static fromJson(json: BlockMoveJson, workspace: Workspace, event?: any):
BlockMove {
const newEvent =
super.fromJson(json, workspace, event ?? new BlockMove()) as BlockMove;
newEvent.oldParentId = json['oldParentId'];
newEvent.oldInputName = json['oldInputName'];
if (json['oldCoordinate']) {
const xy = json['oldCoordinate'].split(',');
newEvent.oldCoordinate = new Coordinate(Number(xy[0]), Number(xy[1]));
}
newEvent.newParentId = json['newParentId'];
newEvent.newInputName = json['newInputName'];
if (json['newCoordinate']) {
const xy = json['newCoordinate'].split(',');
newEvent.newCoordinate = new Coordinate(Number(xy[0]), Number(xy[1]));
}
if (json['recordUndo'] !== undefined) {
newEvent.recordUndo = json['recordUndo'];
}
return newEvent;
}
/** Record the block's new location. Called after the move. */
recordNew() {
const location = this.currentLocation_();
@@ -210,6 +258,9 @@ export class BlockMove extends BlockBase {
}
export interface BlockMoveJson extends BlockBaseJson {
oldParentId?: string;
oldInputName?: string;
oldCoordinate?: string;
newParentId?: string;
newInputName?: string;
newCoordinate?: string;

View File

@@ -14,9 +14,11 @@ goog.declareModuleId('Blockly.Events.BubbleOpen');
import type {AbstractEventJson} from './events_abstract.js';
import type {BlockSvg} from '../block_svg.js';
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js';
/**
@@ -81,11 +83,34 @@ export class BubbleOpen extends UiBase {
* @param json JSON representation.
*/
override fromJson(json: BubbleOpenJson) {
deprecation.warn(
'Blockly.Events.BubbleOpen.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.isOpen = json['isOpen'];
this.bubbleType = json['bubbleType'];
this.blockId = json['blockId'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of BubbleOpen, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: BubbleOpenJson, workspace: Workspace, event?: any):
BubbleOpen {
const newEvent =
super.fromJson(json, workspace, event ?? new BubbleOpen()) as
BubbleOpen;
newEvent.isOpen = json['isOpen'];
newEvent.bubbleType = json['bubbleType'];
newEvent.blockId = json['blockId'];
return newEvent;
}
}
export enum BubbleType {

View File

@@ -13,11 +13,13 @@ import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.Click');
import type {Block} from '../block.js';
import * as deprecation from '../utils/deprecation.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';
import {Workspace} from '../workspace.js';
/**
@@ -77,10 +79,30 @@ export class Click extends UiBase {
* @param json JSON representation.
*/
override fromJson(json: ClickJson) {
deprecation.warn(
'Blockly.Events.Click.prototype.fromJson', 'version 9', 'version 10',
'Blockly.Events.fromJson');
super.fromJson(json);
this.targetType = json['targetType'];
this.blockId = json['blockId'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of Click, but we can't specify that due to the fact that parameters to
* static methods in subclasses must be supertypes of parameters to
* static methods in superclasses.
* @internal
*/
static fromJson(json: ClickJson, workspace: Workspace, event?: any): Click {
const newEvent =
super.fromJson(json, workspace, event ?? new Click()) as Click;
newEvent.targetType = json['targetType'];
newEvent.blockId = json['blockId'];
return newEvent;
}
}
export enum ClickTarget {

View File

@@ -12,6 +12,7 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentBase');
import * as deprecation from '../utils/deprecation.js';
import * as utilsXml from '../utils/xml.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import * as Xml from '../xml.js';
@@ -20,6 +21,7 @@ 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';
import type {Workspace} from '../workspace.js';
/**
@@ -81,10 +83,31 @@ export class CommentBase extends AbstractEvent {
* @param json JSON representation.
*/
override fromJson(json: CommentBaseJson) {
deprecation.warn(
'Blockly.Events.CommentBase.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.commentId = json['commentId'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of CommentBase, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: CommentBaseJson, workspace: Workspace, event?: any):
CommentBase {
const newEvent =
super.fromJson(json, workspace, event ?? new CommentBase()) as
CommentBase;
newEvent.commentId = json['commentId'];
return newEvent;
}
/**
* Helper function for Comment[Create|Delete]
*

View File

@@ -12,11 +12,13 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentChange');
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js';
/**
@@ -78,11 +80,33 @@ export class CommentChange extends CommentBase {
* @param json JSON representation.
*/
override fromJson(json: CommentChangeJson) {
deprecation.warn(
'Blockly.Events.CommentChange.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.oldContents_ = json['oldContents'];
this.newContents_ = json['newContents'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of CommentChange, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: CommentChangeJson, workspace: Workspace, event?: any):
CommentChange {
const newEvent =
super.fromJson(json, workspace, event ?? new CommentChange()) as
CommentChange;
newEvent.oldContents_ = json['oldContents'];
newEvent.newContents_ = json['newContents'];
return newEvent;
}
/**
* Does this event record any change of state?
*

View File

@@ -12,12 +12,14 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentCreate');
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import * as Xml from '../xml.js';
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js';
/**
@@ -67,10 +69,31 @@ export class CommentCreate extends CommentBase {
* @param json JSON representation.
*/
override fromJson(json: CommentCreateJson) {
deprecation.warn(
'Blockly.Events.CommentCreate.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.xml = Xml.textToDom(json['xml']);
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of CommentCreate, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: CommentCreateJson, workspace: Workspace, event?: any):
CommentCreate {
const newEvent =
super.fromJson(json, workspace, event ?? new CommentCreate()) as
CommentCreate;
newEvent.xml = Xml.textToDom(json['xml']);
return newEvent;
}
/**
* Run a creation event.
*

View File

@@ -15,8 +15,10 @@ goog.declareModuleId('Blockly.Events.CommentDelete');
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';
import * as Xml from '../xml.js';
import type {Workspace} from '../workspace.js';
/**
@@ -50,6 +52,44 @@ export class CommentDelete extends CommentBase {
override run(forward: boolean) {
CommentBase.CommentCreateDeleteHelper(this, !forward);
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
override toJson(): CommentDeleteJson {
const json = super.toJson() as CommentDeleteJson;
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;
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of CommentDelete, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: CommentDeleteJson, workspace: Workspace, event?: any):
CommentDelete {
const newEvent =
super.fromJson(json, workspace, event ?? new CommentDelete()) as
CommentDelete;
newEvent.xml = Xml.textToDom(json['xml']);
return newEvent;
}
}
export interface CommentDeleteJson extends CommentBaseJson {
xml: string;
}
registry.register(

View File

@@ -12,12 +12,14 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentMove');
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import {Coordinate} from '../utils/coordinate.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js';
/**
@@ -111,6 +113,9 @@ export class CommentMove extends CommentBase {
* @param json JSON representation.
*/
override fromJson(json: CommentMoveJson) {
deprecation.warn(
'Blockly.Events.CommentMove.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
let xy = json['oldCoordinate'].split(',');
this.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
@@ -118,6 +123,27 @@ export class CommentMove extends CommentBase {
this.newCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of CommentMove, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: CommentMoveJson, workspace: Workspace, event?: any):
CommentMove {
const newEvent =
super.fromJson(json, workspace, event ?? new CommentMove()) as
CommentMove;
let xy = json['oldCoordinate'].split(',');
newEvent.oldCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
xy = json['newCoordinate'].split(',');
newEvent.newCoordinate_ = new Coordinate(Number(xy[0]), Number(xy[1]));
return newEvent;
}
/**
* Does this event record any change of state?
*

View File

@@ -14,6 +14,7 @@ goog.declareModuleId('Blockly.Events.MarkerMove');
import type {Block} from '../block.js';
import {ASTNode} from '../keyboard_nav/ast_node.js';
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {AbstractEventJson} from './events_abstract.js';
@@ -96,12 +97,36 @@ export class MarkerMove extends UiBase {
* @param json JSON representation.
*/
override fromJson(json: MarkerMoveJson) {
deprecation.warn(
'Blockly.Events.MarkerMove.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.isCursor = json['isCursor'];
this.blockId = json['blockId'];
this.oldNode = json['oldNode'];
this.newNode = json['newNode'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of MarkerMove, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: MarkerMoveJson, workspace: Workspace, event?: any):
MarkerMove {
const newEvent =
super.fromJson(json, workspace, event ?? new MarkerMove()) as
MarkerMove;
newEvent.isCursor = json['isCursor'];
newEvent.blockId = json['blockId'];
newEvent.oldNode = json['oldNode'];
newEvent.newNode = json['newNode'];
return newEvent;
}
}
export interface MarkerMoveJson extends AbstractEventJson {

View File

@@ -0,0 +1,37 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js';
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import type {Workspace} from '../workspace.js';
/**
* The base event for an event associated with a procedure.
*/
export abstract class ProcedureBase extends AbstractEvent {
isBlank = false;
constructor(workspace: Workspace, public readonly model: IProcedureModel) {
super();
this.workspaceId = workspace.id;
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureBaseJson {
const json = super.toJson() as ProcedureBaseJson;
json['procedureId'] = this.model.getId();
return json;
}
}
export interface ProcedureBaseJson extends AbstractEventJson {
procedureId: string;
}

View File

@@ -0,0 +1,87 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {ProcedureBase, ProcedureBaseJson} from './events_procedure_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a procedure's return type/status changing.
*/
export class ProcedureChangeReturn extends ProcedureBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_CHANGE_RETURN;
/** The new type(s) the procedure's return has been set to. */
private newTypes: string[]|null;
/**
* @param oldTypes The type(s) the procedure's return was set to before it
* changed.
*/
constructor(
workpace: Workspace, model: IProcedureModel,
public readonly oldTypes: string[]|null) {
super(workpace, model);
this.newTypes = model.getReturnTypes();
}
run(forward: boolean) {
const procedureModel =
this.getEventWorkspace_().getProcedureMap().get(this.model.getId());
if (!procedureModel) {
throw new Error(
'Cannot change the type of a procedure that does not exist ' +
'in the procedure map');
}
if (forward) {
procedureModel.setReturnTypes(this.newTypes);
} else {
procedureModel.setReturnTypes(this.oldTypes);
}
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureChangeReturnJson {
const json = super.toJson() as ProcedureChangeReturnJson;
json['oldTypes'] = this.oldTypes;
return json;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureChangeReturnJson, workspace: Workspace):
ProcedureChangeReturn {
const model = workspace.getProcedureMap().get(json['procedureId']);
if (!model) {
throw new Error(
'Cannot deserialize procedure change return event because the ' +
'target procedure does not exist');
}
return new ProcedureChangeReturn(workspace, model, json['oldTypes']);
}
}
export interface ProcedureChangeReturnJson extends ProcedureBaseJson {
oldTypes: string[]|null;
}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_CHANGE_RETURN,
ProcedureChangeReturn);

View File

@@ -0,0 +1,74 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import {ObservableParameterModel, ObservableProcedureModel} from '../procedures.js';
import * as registry from '../registry.js';
import {loadProcedure, saveProcedure, State as ProcedureState} from '../serialization/procedures.js';
import type {Workspace} from '../workspace.js';
import {ProcedureBase, ProcedureBaseJson} from './events_procedure_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a procedure data model being created.
*/
export class ProcedureCreate extends ProcedureBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_CREATE;
constructor(workspace: Workspace, model: IProcedureModel) {
super(workspace, model);
}
run(forward: boolean) {
const workspace = this.getEventWorkspace_();
const procedureMap = workspace.getProcedureMap();
const procedureModel = procedureMap.get(this.model.getId());
if (forward) {
if (procedureModel) return;
// TODO: This should add the model to the map instead of creating a dupe.
procedureMap.add(new ObservableProcedureModel(
workspace, this.model.getName(), this.model.getId()));
} else {
if (!procedureModel) return;
procedureMap.delete(this.model.getId());
}
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureCreateJson {
const json = super.toJson() as ProcedureCreateJson;
json['model'] = saveProcedure(this.model);
return json;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureCreateJson, workspace: Workspace):
ProcedureCreate {
return new ProcedureCreate(
workspace,
loadProcedure(
ObservableProcedureModel, ObservableParameterModel, json['model'],
workspace));
}
}
export interface ProcedureCreateJson extends ProcedureBaseJson {
model: ProcedureState,
}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_CREATE, ProcedureCreate);

View File

@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import {ObservableProcedureModel} from '../procedures.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {ProcedureBase, ProcedureBaseJson} from './events_procedure_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a procedure data model being deleted.
*/
export class ProcedureDelete extends ProcedureBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_DELETE;
constructor(workspace: Workspace, model: IProcedureModel) {
super(workspace, model);
}
run(forward: boolean) {
const workspace = this.getEventWorkspace_();
const procedureMap = workspace.getProcedureMap();
const procedureModel = procedureMap.get(this.model.getId());
if (forward) {
if (!procedureModel) return;
procedureMap.delete(this.model.getId());
} else {
if (procedureModel) return;
procedureMap.add(new ObservableProcedureModel(
workspace, this.model.getName(), this.model.getId()));
}
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureDeleteJson {
return super.toJson() as ProcedureDeleteJson;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureDeleteJson, workspace: Workspace):
ProcedureDelete {
const model = workspace.getProcedureMap().get(json['procedureId']);
if (!model) {
throw new Error(
'Cannot deserialize procedure delete event because the ' +
'target procedure does not exist');
}
return new ProcedureDelete(workspace, model);
}
}
export interface ProcedureDeleteJson extends ProcedureBaseJson {}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_DELETE, ProcedureDelete);

View File

@@ -0,0 +1,76 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {ProcedureBase, ProcedureBaseJson} from './events_procedure_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a procedure data model being enabled or disabled.
*/
export class ProcedureEnable extends ProcedureBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_ENABLE;
private oldState: boolean;
private newState: boolean;
constructor(workspace: Workspace, model: IProcedureModel) {
super(workspace, model);
this.oldState = !model.getEnabled();
this.newState = model.getEnabled();
}
run(forward: boolean) {
const procedureModel =
this.getEventWorkspace_().getProcedureMap().get(this.model.getId());
if (!procedureModel) {
throw new Error(
'Cannot change the enabled state of a procedure that does not ' +
'exist in the procedure map');
}
if (forward) {
procedureModel.setEnabled(this.newState);
} else {
procedureModel.setEnabled(this.oldState);
}
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureEnableJson {
return super.toJson() as ProcedureEnableJson;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureEnableJson, workspace: Workspace):
ProcedureEnable {
const model = workspace.getProcedureMap().get(json['procedureId']);
if (!model) {
throw new Error(
'Cannot deserialize procedure enable event because the ' +
'target procedure does not exist');
}
return new ProcedureEnable(workspace, model);
}
}
export interface ProcedureEnableJson extends ProcedureBaseJson {}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_ENABLE, ProcedureEnable);

View File

@@ -0,0 +1,39 @@
/**
* @license
* Copyright 2022 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 {ProcedureBase, ProcedureBaseJson} from './events_procedure_base.js';
import type {Workspace} from '../workspace.js';
/**
* The base event for an event associated with a procedure parameter.
*/
export abstract class ProcedureParameterBase extends ProcedureBase {
constructor(
workspace: Workspace, model: IProcedureModel,
public readonly parameter: IParameterModel) {
super(workspace, model);
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureParameterBaseJson {
const json = super.toJson() as ProcedureParameterBaseJson;
json['parameterId'] = this.model.getId();
return json;
}
}
export interface ProcedureParameterBaseJson extends ProcedureBaseJson {
parameterId: string,
}

View File

@@ -0,0 +1,101 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IParameterModel} from '../interfaces/i_parameter_model.js';
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import {ObservableParameterModel} from '../procedures/observable_parameter_model.js';
import * as registry from '../registry.js';
import {loadParameter, ParameterState, saveParameter} from '../serialization/procedures.js';
import type {Workspace} from '../workspace.js';
import {ProcedureParameterBase, ProcedureParameterBaseJson} from './events_procedure_parameter_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a parameter being added to a procedure.
*/
export class ProcedureParameterCreate extends ProcedureParameterBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_PARAMETER_CREATE;
/**
* @param parameter The parameter model that was just added to the procedure.
* @param index The index the parameter was inserted at.
*/
constructor(
workspace: Workspace, procedure: IProcedureModel,
parameter: IParameterModel, public readonly index: number) {
super(workspace, procedure, parameter);
}
run(forward: boolean) {
const workspace = this.getEventWorkspace_();
const procedureMap = workspace.getProcedureMap();
const procedureModel = procedureMap.get(this.model.getId());
if (!procedureModel) {
throw new Error(
'Cannot add a parameter to a procedure that does not exist ' +
'in the procedure map');
}
const parameterModel = procedureModel.getParameter(this.index);
if (forward) {
if (this.parameterMatches(parameterModel)) return;
// TODO: This should just add the parameter instead of creating a dupe.
procedureModel.insertParameter(
new ObservableParameterModel(
workspace, this.parameter.getName(), this.parameter.getId()),
this.index);
} else {
if (!this.parameterMatches(parameterModel)) return;
procedureModel.deleteParameter(this.index);
}
}
parameterMatches(param: IParameterModel) {
return param && param.getId() === this.parameter.getId();
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureParameterCreateJson {
const json = super.toJson() as ProcedureParameterCreateJson;
json['parameter'] = saveParameter(this.parameter);
json['index'] = this.index;
return json;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureParameterCreateJson, workspace: Workspace):
ProcedureParameterCreate {
const procedure = workspace.getProcedureMap().get(json['procedureId']);
if (!procedure) {
throw new Error(
'Cannot deserialize parameter create event because the ' +
'target procedure does not exist');
}
return new ProcedureParameterCreate(
workspace, procedure,
loadParameter(ObservableParameterModel, json['parameter'], workspace),
json['index']);
}
}
export interface ProcedureParameterCreateJson extends
ProcedureParameterBaseJson {
parameter: ParameterState, index: number,
}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_PARAMETER_CREATE,
ProcedureParameterCreate);

View File

@@ -0,0 +1,97 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IParameterModel} from '../interfaces/i_parameter_model.js';
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import {ObservableParameterModel} from '../procedures/observable_parameter_model.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {ProcedureParameterBase, ProcedureParameterBaseJson} from './events_procedure_parameter_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a parameter being removed from a procedure.
*/
export class ProcedureParameterDelete extends ProcedureParameterBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_PARAMETER_DELETE;
/**
* @param parameter The parameter model that was just removed from the
* procedure.
* @param index The index the parameter was at before it was removed.
*/
constructor(
workspace: Workspace, procedure: IProcedureModel,
parameter: IParameterModel, public readonly index: number) {
super(workspace, procedure, parameter);
}
run(forward: boolean) {
const workspace = this.getEventWorkspace_();
const procedureMap = workspace.getProcedureMap();
const procedureModel = procedureMap.get(this.model.getId());
if (!procedureModel) {
throw new Error(
'Cannot add a parameter to a procedure that does not exist ' +
'in the procedure map');
}
const parameterModel = procedureModel.getParameter(this.index);
if (forward) {
if (!this.parameterMatches(parameterModel)) return;
procedureModel.deleteParameter(this.index);
} else {
if (this.parameterMatches(parameterModel)) return;
// TODO: this should just insert the model instead of creating a dupe.
procedureModel.insertParameter(
new ObservableParameterModel(
workspace, this.parameter.getName(), this.parameter.getId()),
this.index);
}
}
parameterMatches(param: IParameterModel) {
return param && param.getId() === this.parameter.getId();
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureParameterDeleteJson {
const json = super.toJson() as ProcedureParameterDeleteJson;
json['index'] = this.index;
return json;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureParameterDeleteJson, workspace: Workspace):
ProcedureParameterDelete {
const model = workspace.getProcedureMap().get(json['procedureId']);
if (!model) {
throw new Error(
'Cannot deserialize procedure delete event because the ' +
'target procedure does not exist');
}
const param = model.getParameter(json['index']);
return new ProcedureParameterDelete(workspace, model, param, json['index']);
}
}
export interface ProcedureParameterDeleteJson extends
ProcedureParameterBaseJson {
index: number;
}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_PARAMETER_DELETE,
ProcedureParameterDelete);

View File

@@ -0,0 +1,102 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IParameterModel} from '../interfaces/i_parameter_model.js';
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {ProcedureParameterBase, ProcedureParameterBaseJson} from './events_procedure_parameter_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a parameter of a procedure being renamed.
*/
export class ProcedureParameterRename extends ProcedureParameterBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_PARAMETER_RENAME;
private readonly newName: string;
constructor(
workspace: Workspace, procedure: IProcedureModel,
parameter: IParameterModel, public readonly oldName: string) {
super(workspace, procedure, parameter);
this.newName = parameter.getName();
}
run(forward: boolean) {
const parameterModel = findMatchingParameter(
this.getEventWorkspace_(), this.model.getId(), this.parameter.getId());
if (!parameterModel) {
throw new Error(
'Cannot rename a parameter that does not exist ' +
'in the procedure map');
}
if (forward) {
parameterModel.setName(this.newName);
} else {
parameterModel.setName(this.oldName);
}
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureParameterRenameJson {
const json = super.toJson() as ProcedureParameterRenameJson;
json['oldName'] = this.oldName;
return json;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureParameterRenameJson, workspace: Workspace):
ProcedureParameterRename {
const model = workspace.getProcedureMap().get(json['procedureId']);
if (!model) {
throw new Error(
'Cannot deserialize procedure delete event because the ' +
'target procedure does not exist');
}
const param = findMatchingParameter(
workspace, json['procedureId'], json['parameterId']);
if (!param) {
throw new Error(
'Cannot deserialize parameter rename event because the ' +
'target parameter does not exist');
}
return new ProcedureParameterRename(
workspace, model, param, json['oldName']);
}
}
function findMatchingParameter(
workspace: Workspace, modelId: string, paramId: string): IParameterModel|
undefined {
const procedureModel = workspace.getProcedureMap().get(modelId);
if (!procedureModel) {
throw new Error(
'Cannot rename the parameter of a procedure that does not exist ' +
'in the procedure map');
}
return procedureModel.getParameters().find((p) => p.getId() === paramId);
}
export interface ProcedureParameterRenameJson extends
ProcedureParameterBaseJson {
oldName: string;
}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_PARAMETER_RENAME,
ProcedureParameterRename);

View File

@@ -0,0 +1,78 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {ProcedureBase, ProcedureBaseJson} from './events_procedure_base.js';
import * as eventUtils from './utils.js';
/**
* Represents a procedure being renamed.
*/
export class ProcedureRename extends ProcedureBase {
/** A string used to check the type of the event. */
type = eventUtils.PROCEDURE_RENAME;
private newName: string;
constructor(
workspace: Workspace, model: IProcedureModel,
public readonly oldName: string) {
super(workspace, model);
this.newName = model.getName();
}
run(forward: boolean) {
const procedureModel =
this.getEventWorkspace_().getProcedureMap().get(this.model.getId());
if (!procedureModel) {
throw new Error(
'Cannot change the type of a procedure that does not exist ' +
'in the procedure map');
}
if (forward) {
procedureModel.setName(this.newName);
} else {
procedureModel.setName(this.oldName);
}
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
toJson(): ProcedureRenameJson {
const json = super.toJson() as ProcedureRenameJson;
json['oldName'] = this.oldName;
return json;
}
/**
* Deserializes the JSON event.
*
* @internal
*/
static fromJson(json: ProcedureRenameJson, workspace: Workspace):
ProcedureRename {
const model = workspace.getProcedureMap().get(json['procedureId']);
if (!model) {
throw new Error(
'Cannot deserialize procedure rename event because the ' +
'target procedure does not exist');
}
return new ProcedureRename(workspace, model, json['oldName']);
}
}
export interface ProcedureRenameJson extends ProcedureBaseJson {
oldName: string;
}
registry.register(
registry.Type.EVENT, eventUtils.PROCEDURE_RENAME, ProcedureRename);

View File

@@ -12,11 +12,13 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.Selected');
import * as deprecation from '../utils/deprecation.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';
import type {Workspace} from '../workspace.js';
/**
@@ -67,10 +69,31 @@ export class Selected extends UiBase {
* @param json JSON representation.
*/
override fromJson(json: SelectedJson) {
deprecation.warn(
'Blockly.Events.Selected.prototype.fromJson', 'version 9', 'version 10',
'Blockly.Events.fromJson');
super.fromJson(json);
this.oldElementId = json['oldElementId'];
this.newElementId = json['newElementId'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of Selected, but we can't specify that due to the fact that parameters
* to static methods in subclasses must be supertypes of parameters to
* static methods in superclasses.
* @internal
*/
static fromJson(json: SelectedJson, workspace: Workspace, event?: any):
Selected {
const newEvent =
super.fromJson(json, workspace, event ?? new Selected()) as Selected;
newEvent.oldElementId = json['oldElementId'];
newEvent.newElementId = json['newElementId'];
return newEvent;
}
}
export interface SelectedJson extends AbstractEventJson {

View File

@@ -12,10 +12,12 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.ThemeChange');
import * as deprecation from '../utils/deprecation.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';
import type {Workspace} from '../workspace.js';
/**
@@ -61,9 +63,30 @@ export class ThemeChange extends UiBase {
* @param json JSON representation.
*/
override fromJson(json: ThemeChangeJson) {
deprecation.warn(
'Blockly.Events.ThemeChange.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.themeName = json['themeName'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of ThemeChange, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: ThemeChangeJson, workspace: Workspace, event?: any):
ThemeChange {
const newEvent =
super.fromJson(json, workspace, event ?? new ThemeChange()) as
ThemeChange;
newEvent.themeName = json['themeName'];
return newEvent;
}
}
export interface ThemeChangeJson extends AbstractEventJson {

View File

@@ -12,10 +12,12 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.ToolboxItemSelect');
import * as deprecation from '../utils/deprecation.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';
import type {Workspace} from '../workspace.js';
/**
@@ -66,10 +68,33 @@ export class ToolboxItemSelect extends UiBase {
* @param json JSON representation.
*/
override fromJson(json: ToolboxItemSelectJson) {
deprecation.warn(
'Blockly.Events.ToolboxItemSelect.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.oldItem = json['oldItem'];
this.newItem = json['newItem'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of ToolboxItemSelect, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(
json: ToolboxItemSelectJson, workspace: Workspace,
event?: any): ToolboxItemSelect {
const newEvent =
super.fromJson(json, workspace, event ?? new ToolboxItemSelect()) as
ToolboxItemSelect;
newEvent.oldItem = json['oldItem'];
newEvent.newItem = json['newItem'];
return newEvent;
}
}
export interface ToolboxItemSelectJson extends AbstractEventJson {

View File

@@ -12,11 +12,13 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.TrashcanOpen');
import * as deprecation from '../utils/deprecation.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';
import type {Workspace} from '../workspace.js';
/**
@@ -63,9 +65,30 @@ export class TrashcanOpen extends UiBase {
* @param json JSON representation.
*/
override fromJson(json: TrashcanOpenJson) {
deprecation.warn(
'Blockly.Events.TrashcanOpen.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.isOpen = json['isOpen'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of TrashcanOpen, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(json: TrashcanOpenJson, workspace: Workspace, event?: any):
TrashcanOpen {
const newEvent =
super.fromJson(json, workspace, event ?? new TrashcanOpen()) as
TrashcanOpen;
newEvent.isOpen = json['isOpen'];
return newEvent;
}
}
export interface TrashcanOpenJson extends AbstractEventJson {

View File

@@ -12,9 +12,11 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarBase');
import * as deprecation from '../utils/deprecation.js';
import type {VariableModel} from '../variable_model.js';
import {Abstract as AbstractEvent, AbstractEventJson} from './events_abstract.js';
import type {Workspace} from '../workspace.js';
/**
@@ -64,9 +66,29 @@ export class VarBase extends AbstractEvent {
* @param json JSON representation.
*/
override fromJson(json: VarBaseJson) {
deprecation.warn(
'Blockly.Events.VarBase.prototype.fromJson', 'version 9', 'version 10',
'Blockly.Events.fromJson');
super.fromJson(json);
this.varId = json['varId'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of VarBase, but we can't specify that due to the fact that parameters
* to static methods in subclasses must be supertypes of parameters to
* static methods in superclasses.
* @internal
*/
static fromJson(json: VarBaseJson, workspace: Workspace, event?: any):
VarBase {
const newEvent =
super.fromJson(json, workspace, event ?? new VarBase()) as VarBase;
newEvent.varId = json['varId'];
return newEvent;
}
}
export interface VarBaseJson extends AbstractEventJson {

View File

@@ -12,11 +12,13 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarCreate');
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import type {VariableModel} from '../variable_model.js';
import {VarBase, VarBaseJson} from './events_var_base.js';
import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js';
/**
@@ -70,11 +72,32 @@ export class VarCreate extends VarBase {
* @param json JSON representation.
*/
override fromJson(json: VarCreateJson) {
deprecation.warn(
'Blockly.Events.VarCreate.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.varType = json['varType'];
this.varName = json['varName'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of VarCreate, but we can't specify that due to the fact that parameters
* to static methods in subclasses must be supertypes of parameters to
* static methods in superclasses.
* @internal
*/
static fromJson(json: VarCreateJson, workspace: Workspace, event?: any):
VarCreate {
const newEvent =
super.fromJson(json, workspace, event ?? new VarCreate()) as VarCreate;
newEvent.varType = json['varType'];
newEvent.varName = json['varName'];
return newEvent;
}
/**
* Run a variable creation event.
*

View File

@@ -12,11 +12,13 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarDelete');
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import type {VariableModel} from '../variable_model.js';
import {VarBase, VarBaseJson} from './events_var_base.js';
import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js';
/**
@@ -70,11 +72,32 @@ export class VarDelete extends VarBase {
* @param json JSON representation.
*/
override fromJson(json: VarDeleteJson) {
deprecation.warn(
'Blockly.Events.VarDelete.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.varType = json['varType'];
this.varName = json['varName'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of VarDelete, but we can't specify that due to the fact that parameters
* to static methods in subclasses must be supertypes of parameters to
* static methods in superclasses.
* @internal
*/
static fromJson(json: VarDeleteJson, workspace: Workspace, event?: any):
VarDelete {
const newEvent =
super.fromJson(json, workspace, event ?? new VarDelete()) as VarDelete;
newEvent.varType = json['varType'];
newEvent.varName = json['varName'];
return newEvent;
}
/**
* Run a variable deletion event.
*

View File

@@ -12,11 +12,13 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarRename');
import * as deprecation from '../utils/deprecation.js';
import * as registry from '../registry.js';
import type {VariableModel} from '../variable_model.js';
import {VarBase, VarBaseJson} from './events_var_base.js';
import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js';
/**
@@ -71,11 +73,32 @@ export class VarRename extends VarBase {
* @param json JSON representation.
*/
override fromJson(json: VarRenameJson) {
deprecation.warn(
'Blockly.Events.VarRename.prototype.fromJson', 'version 9',
'version 10', 'Blockly.Events.fromJson');
super.fromJson(json);
this.oldName = json['oldName'];
this.newName = json['newName'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of VarRename, but we can't specify that due to the fact that parameters
* to static methods in subclasses must be supertypes of parameters to
* static methods in superclasses.
* @internal
*/
static fromJson(json: VarRenameJson, workspace: Workspace, event?: any):
VarRename {
const newEvent =
super.fromJson(json, workspace, event ?? new VarRename()) as VarRename;
newEvent.oldName = json['oldName'];
newEvent.newName = json['newName'];
return newEvent;
}
/**
* Run a variable rename event.
*

View File

@@ -12,10 +12,12 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.ViewportChange');
import * as deprecation from '../utils/deprecation.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';
import type {Workspace} from '../workspace.js';
/**
@@ -105,12 +107,36 @@ export class ViewportChange extends UiBase {
* @param json JSON representation.
*/
override fromJson(json: ViewportChangeJson) {
deprecation.warn(
'Blockly.Events.Viewport.prototype.fromJson', 'version 9', 'version 10',
'Blockly.Events.fromJson');
super.fromJson(json);
this.viewTop = json['viewTop'];
this.viewLeft = json['viewLeft'];
this.scale = json['scale'];
this.oldScale = json['oldScale'];
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of Viewport, but we can't specify that due to the fact that parameters
* to static methods in subclasses must be supertypes of parameters to
* static methods in superclasses.
* @internal
*/
static fromJson(json: ViewportChangeJson, workspace: Workspace, event?: any):
ViewportChange {
const newEvent =
super.fromJson(json, workspace, event ?? new ViewportChange()) as
ViewportChange;
newEvent.viewTop = json['viewTop'];
newEvent.viewLeft = json['viewLeft'];
newEvent.scale = json['scale'];
newEvent.oldScale = json['oldScale'];
return newEvent;
}
}
export interface ViewportChangeJson extends AbstractEventJson {

View File

@@ -240,6 +240,30 @@ export const COMMENT_MOVE = 'comment_move';
*/
export const FINISHED_LOADING = 'finished_loading';
/** Name of event that creates a procedure model. */
export const PROCEDURE_CREATE = 'procedure_create';
/** Name of event that deletes a procedure model. */
export const PROCEDURE_DELETE = 'procedure_delete';
/** Name of event that renames a procedure model. */
export const PROCEDURE_RENAME = 'procedure_rename';
/** Name of event that enables/disables a procedure model. */
export const PROCEDURE_ENABLE = 'procedure_enable';
/** Name of event that changes the returntype of a procedure model. */
export const PROCEDURE_CHANGE_RETURN = 'procedure_change_return';
/** Name of event that creates a procedure parameter. */
export const PROCEDURE_PARAMETER_CREATE = 'procedure_parameter_create';
/** Name of event that deletes a procedure parameter. */
export const PROCEDURE_PARAMETER_DELETE = 'procedure_parameter_delete';
/** Name of event that renames a procedure parameter. */
export const PROCEDURE_PARAMETER_RENAME = 'procedure_parameter_rename';
/**
* Type of events that cause objects to be bumped back into the visible
* portion of the workspace.
@@ -494,15 +518,33 @@ export function getDescendantIds(block: Block): string[] {
export function fromJson(
json: AnyDuringMigration, workspace: Workspace): Abstract {
const eventClass = get(json['type']);
if (!eventClass) {
throw Error('Unknown event type.');
if (!eventClass) throw Error('Unknown event type.');
if (eventClassHasStaticFromJson(eventClass)) {
return (eventClass as any).fromJson(json, workspace);
}
// Fallback to the old deserialization method.
const event = new eventClass();
event.fromJson(json);
event.workspaceId = workspace.id;
return event;
}
/**
* Returns true if the given event constructor has /its own/ static fromJson
* method.
*
* Returns false if no static fromJson method exists on the contructor, or if
* the static fromJson method is inheritted.
*/
function eventClassHasStaticFromJson(eventClass: new (...p: any[]) => Abstract):
boolean {
const untypedEventClass = eventClass as any;
return Object.getOwnPropertyDescriptors(untypedEventClass).fromJson &&
typeof untypedEventClass.fromJson === 'function';
}
/**
* Gets the class for a specific event type from the registry.
*

View File

@@ -45,15 +45,28 @@ import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import * as Xml from './xml.js';
export type FieldValidator<T = any> = (value?: T) => T|null|undefined;
/**
* Abstract class for an editable field.
*
* @alias Blockly.Field
*/
export abstract class Field implements IASTNodeLocationSvg,
IASTNodeLocationWithBlock,
IKeyboardAccessible, IRegistrable {
export abstract class Field<T = any> implements IASTNodeLocationSvg,
IASTNodeLocationWithBlock,
IKeyboardAccessible,
IRegistrable {
/**
* To overwrite the default value which is set in **Field**, directly update
* the prototype.
*
* Example:
* ```typescript
* FieldImage.prototype.DEFAULT_VALUE = null;
* ```
*/
DEFAULT_VALUE: T|null = null;
/** Non-breaking space. */
static readonly NBSP = '\u00A0';
@@ -69,10 +82,10 @@ export abstract class Field implements IASTNodeLocationSvg,
* Static labels are usually unnamed.
*/
name?: string = undefined;
protected value_: AnyDuringMigration;
protected value_: T|null;
/** Validation function called when user edits an editable field. */
protected validator_: Function|null = null;
protected validator_: FieldValidator<T>|null = null;
/**
* Used to cache the field's tooltip value if setTooltip is called when the
@@ -181,15 +194,15 @@ export abstract class Field implements IASTNodeLocationSvg,
* this parameter supports.
*/
constructor(
value: AnyDuringMigration, opt_validator?: Function|null,
value: T|Sentinel, opt_validator?: FieldValidator<T>|null,
opt_config?: FieldConfig) {
/**
* A generic value possessed by the field.
* Should generally be non-null, only null when the field is created.
*/
this.value_ = ('DEFAULT_VALUE' in (new.target).prototype) ?
((new.target).prototype as AnyDuringMigration).DEFAULT_VALUE :
null;
this.value_ = 'DEFAULT_VALUE' in new.target.prototype ?
new.target.prototype.DEFAULT_VALUE :
this.DEFAULT_VALUE;
/** The size of the area rendered by the field. */
this.size_ = new Size(0, 0);
@@ -348,7 +361,7 @@ export abstract class Field implements IASTNodeLocationSvg,
if (!clickTarget) throw new Error('A click target has not been set.');
Tooltip.bindMouseEvents(clickTarget);
this.mouseDownWrapper_ = browserEvents.conditionalBind(
clickTarget, 'mousedown', this, this.onMouseDown_);
clickTarget, 'pointerdown', this, this.onMouseDown_);
}
/**
@@ -597,7 +610,7 @@ export abstract class Field implements IASTNodeLocationSvg,
* @param handler The validator function or null to clear a previous
* validator.
*/
setValidator(handler: Function) {
setValidator(handler: FieldValidator<T>) {
this.validator_ = handler;
}
@@ -1063,17 +1076,17 @@ export abstract class Field implements IASTNodeLocationSvg,
// NOP
/**
* Handle a mouse down event on a field.
* Handle a pointerdown event on a field.
*
* @param e Mouse down event.
* @param e Pointer down event.
*/
protected onMouseDown_(e: Event) {
protected onMouseDown_(e: PointerEvent) {
if (!this.sourceBlock_ || this.sourceBlock_.isDeadOrDying()) {
return;
}
const gesture = (this.sourceBlock_.workspace as WorkspaceSvg).getGesture(e);
if (gesture) {
gesture.setStartField(this);
gesture.setStartField(this as Field);
}
}

View File

@@ -18,7 +18,7 @@ import * as Css from './css.js';
import * as dropDownDiv from './dropdowndiv.js';
import {Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {FieldTextInputConfig, FieldTextInput} from './field_textinput.js';
import {FieldInput, FieldInputConfig, FieldInputValidator} from './field_input.js';
import * as dom from './utils/dom.js';
import {KeyCodes} from './utils/keycodes.js';
import * as math from './utils/math.js';
@@ -27,16 +27,14 @@ import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
export type FieldAngleValidator = FieldInputValidator<number>;
/**
* Class for an editable angle field.
*
* @alias Blockly.FieldAngle
*/
export class FieldAngle extends FieldTextInput {
/** The default value for this field. */
// protected override DEFAULT_VALUE = 0;
export class FieldAngle extends FieldInput<number> {
/**
* The default amount to round angles to when using a mouse or keyboard nav
* input. Must be a positive integer to support keyboard navigation.
@@ -135,7 +133,7 @@ export class FieldAngle extends FieldTextInput {
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|number|Sentinel, opt_validator?: Function,
opt_value?: string|number|Sentinel, opt_validator?: FieldAngleValidator,
opt_config?: FieldAngleConfig) {
super(Field.SKIP_SETUP);
@@ -280,9 +278,9 @@ export class FieldAngle extends FieldTextInput {
// a click handler on the drag surface to update the value if the surface
// is clicked.
this.clickSurfaceWrapper_ = browserEvents.conditionalBind(
circle, 'click', this, this.onMouseMove_, true, true);
circle, 'pointerdown', this, this.onMouseMove_, true);
this.moveSurfaceWrapper_ = browserEvents.conditionalBind(
circle, 'mousemove', this, this.onMouseMove_, true, true);
circle, 'pointermove', this, this.onMouseMove_, true);
this.editor_ = svg;
}
@@ -315,15 +313,11 @@ export class FieldAngle extends FieldTextInput {
*
* @param e Mouse move event.
*/
protected onMouseMove_(e: Event) {
protected onMouseMove_(e: PointerEvent) {
// Calculate angle.
const bBox = this.gauge_!.ownerSVGElement!.getBoundingClientRect();
// AnyDuringMigration because: Property 'clientX' does not exist on type
// 'Event'.
const dx = (e as AnyDuringMigration).clientX - bBox.left - FieldAngle.HALF;
// AnyDuringMigration because: Property 'clientY' does not exist on type
// 'Event'.
const dy = (e as AnyDuringMigration).clientY - bBox.top - FieldAngle.HALF;
const dx = e.clientX - bBox.left - FieldAngle.HALF;
const dy = e.clientY - bBox.top - FieldAngle.HALF;
let angle = Math.atan(-dy / dx);
if (isNaN(angle)) {
// This shouldn't happen, but let's not let this error propagate further.
@@ -480,7 +474,7 @@ export class FieldAngle extends FieldTextInput {
* @nocollapse
* @internal
*/
static override fromJson(options: FieldAngleFromJsonConfig): FieldAngle {
static fromJson(options: FieldAngleFromJsonConfig): FieldAngle {
// `this` might be a subclass of FieldAngle if that class doesn't override
// the static fromJson method.
return new this(options.angle, undefined, options);
@@ -517,7 +511,7 @@ Css.register(`
fieldRegistry.register('field_angle', FieldAngle);
(FieldAngle.prototype as AnyDuringMigration).DEFAULT_VALUE = 0;
FieldAngle.prototype.DEFAULT_VALUE = 0;
/**
* The two main modes of the angle field.
@@ -541,7 +535,7 @@ export enum Mode {
/**
* Extra configuration options for the angle field.
*/
export interface FieldAngleConfig extends FieldTextInputConfig {
export interface FieldAngleConfig extends FieldInputConfig {
mode?: Mode;
clockwise?: boolean;
offset?: number;

View File

@@ -16,17 +16,18 @@ goog.declareModuleId('Blockly.FieldCheckbox');
import './events/events_block_change.js';
import * as dom from './utils/dom.js';
import {FieldConfig, Field} from './field.js';
import {Field, FieldConfig, FieldValidator} from './field.js';
import * as fieldRegistry from './field_registry.js';
import type {Sentinel} from './utils/sentinel.js';
export type FieldCheckboxValidator = FieldValidator<boolean>;
/**
* Class for a checkbox field.
*
* @alias Blockly.FieldCheckbox
*/
export class FieldCheckbox extends Field {
export class FieldCheckbox extends Field<boolean> {
/** Default character for the checkmark. */
static readonly CHECK_CHAR = '✓';
private checkChar_: string;
@@ -58,7 +59,8 @@ export class FieldCheckbox extends Field {
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|boolean|Sentinel, opt_validator?: Function,
opt_value?: string|boolean|Sentinel,
opt_validator?: FieldCheckboxValidator,
opt_config?: FieldCheckboxConfig) {
super(Field.SKIP_SETUP);
@@ -236,7 +238,7 @@ export class FieldCheckbox extends Field {
fieldRegistry.register('field_checkbox', FieldCheckbox);
(FieldCheckbox.prototype as AnyDuringMigration).DEFAULT_VALUE = false;
FieldCheckbox.prototype.DEFAULT_VALUE = false;
/**
* Config options for the checkbox field.

View File

@@ -20,7 +20,7 @@ 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 {Field, FieldConfig, FieldValidator} from './field.js';
import * as fieldRegistry from './field_registry.js';
import * as aria from './utils/aria.js';
import * as colour from './utils/colour.js';
@@ -29,13 +29,14 @@ import {KeyCodes} from './utils/keycodes.js';
import type {Sentinel} from './utils/sentinel.js';
import {Size} from './utils/size.js';
export type FieldColourValidator = FieldValidator<string>;
/**
* Class for a colour input field.
*
* @alias Blockly.FieldColour
*/
export class FieldColour extends Field {
export class FieldColour extends Field<string> {
/**
* An array of colour strings for the palette.
* Copied from goog.ui.ColorPicker.SIMPLE_GRID_COLORS
@@ -152,7 +153,7 @@ export class FieldColour extends Field {
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|Sentinel, opt_validator?: Function,
opt_value?: string|Sentinel, opt_validator?: FieldColourValidator,
opt_config?: FieldColourConfig) {
super(Field.SKIP_SETUP);
@@ -305,7 +306,7 @@ export class FieldColour extends Field {
*
* @param e Mouse event.
*/
private onClick_(e: MouseEvent) {
private onClick_(e: PointerEvent) {
const cell = e.target as Element;
const colour = cell && cell.getAttribute('data-colour');
if (colour !== null) {
@@ -414,7 +415,7 @@ export class FieldColour extends Field {
*
* @param e Mouse event.
*/
private onMouseMove_(e: MouseEvent) {
private onMouseMove_(e: PointerEvent) {
const cell = e.target as Element;
const index = cell && Number(cell.getAttribute('data-index'));
if (index !== null && index !== this.highlightedIndex_) {
@@ -533,13 +534,13 @@ export class FieldColour extends Field {
// Configure event handler on the table to listen for any event in a cell.
this.onClickWrapper_ = browserEvents.conditionalBind(
table, 'click', this, this.onClick_, true);
table, 'pointerdown', this, this.onClick_, true);
this.onMouseMoveWrapper_ = browserEvents.conditionalBind(
table, 'mousemove', this, this.onMouseMove_, true);
table, 'pointermove', this, this.onMouseMove_, true);
this.onMouseEnterWrapper_ = browserEvents.conditionalBind(
table, 'mouseenter', this, this.onMouseEnter_, true);
table, 'pointerenter', this, this.onMouseEnter_, true);
this.onMouseLeaveWrapper_ = browserEvents.conditionalBind(
table, 'mouseleave', this, this.onMouseLeave_, true);
table, 'pointerleave', this, this.onMouseLeave_, true);
this.onKeyDownWrapper_ =
browserEvents.conditionalBind(table, 'keydown', this, this.onKeyDown_);
@@ -588,10 +589,7 @@ export class FieldColour extends Field {
}
/** The default value for this field. */
// AnyDuringMigration because: Property 'DEFAULT_VALUE' is protected and only
// accessible within class 'FieldColour' and its subclasses.
(FieldColour.prototype as AnyDuringMigration).DEFAULT_VALUE =
FieldColour.COLOURS[0];
FieldColour.prototype.DEFAULT_VALUE = FieldColour.COLOURS[0];
/** CSS for colour picker. See css.js for use. */
Css.register(`

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, UnattachedFieldError} from './field.js';
import {Field, FieldConfig, FieldValidator, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {Menu} from './menu.js';
import {MenuItem} from './menuitem.js';
@@ -28,12 +28,14 @@ import type {Sentinel} from './utils/sentinel.js';
import * as utilsString from './utils/string.js';
import {Svg} from './utils/svg.js';
export type FieldDropdownValidator = FieldValidator<string>;
/**
* Class for an editable dropdown field.
*
* @alias Blockly.FieldDropdown
*/
export class FieldDropdown extends Field {
export class FieldDropdown extends Field<string> {
/** Horizontal distance that a checkmark overhangs the dropdown. */
static CHECKMARK_OVERHANG = 25;
@@ -111,13 +113,13 @@ export class FieldDropdown extends Field {
*/
constructor(
menuGenerator: MenuGenerator,
opt_validator?: Function,
opt_validator?: FieldDropdownValidator,
opt_config?: FieldConfig,
);
constructor(menuGenerator: Sentinel);
constructor(
menuGenerator: MenuGenerator|Sentinel,
opt_validator?: Function,
opt_validator?: FieldDropdownValidator,
opt_config?: FieldConfig,
) {
super(Field.SKIP_SETUP);

View File

@@ -12,7 +12,7 @@
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldImage');
import {FieldConfig, Field} from './field.js';
import {Field, FieldConfig} from './field.js';
import * as fieldRegistry from './field_registry.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';
@@ -20,13 +20,12 @@ import type {Sentinel} from './utils/sentinel.js';
import {Size} from './utils/size.js';
import {Svg} from './utils/svg.js';
/**
* Class for an image on a block.
*
* @alias Blockly.FieldImage
*/
export class FieldImage extends Field {
export class FieldImage extends Field<string> {
/**
* Vertical padding below the image, which is included in the reported height
* of the field.
@@ -269,7 +268,7 @@ export class FieldImage extends Field {
fieldRegistry.register('field_image', FieldImage);
(FieldImage.prototype as AnyDuringMigration).DEFAULT_VALUE = '';
FieldImage.prototype.DEFAULT_VALUE = '';
/**
* Config options for the image field.

593
core/field_input.ts Normal file
View File

@@ -0,0 +1,593 @@
/**
* @license
* Copyright 2012 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Text input field.
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldInput');
// Unused import preserved for side-effects. Remove if unneeded.
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 {Field, FieldConfig, FieldValidator, UnattachedFieldError} from './field.js';
import {Msg} from './msg.js';
import * as aria from './utils/aria.js';
import {Coordinate} from './utils/coordinate.js';
import {KeyCodes} from './utils/keycodes.js';
import type {Sentinel} from './utils/sentinel.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js';
export type InputTypes = string|number;
export type FieldInputValidator<T extends InputTypes> = FieldValidator<T>;
/**
* Class for an editable text field.
*
* @alias Blockly.FieldInput
*/
export abstract class FieldInput<T extends InputTypes> extends Field<T> {
/**
* Pixel size of input border radius.
* Should match blocklyText's border-radius in CSS.
*/
static BORDERRADIUS = 4;
/** Allow browser to spellcheck this field. */
protected spellcheck_ = true;
/** The HTML input element. */
protected htmlInput_: HTMLInputElement|null = null;
/** True if the field's value is currently being edited via the UI. */
protected isBeingEdited_ = false;
/**
* True if the value currently displayed in the field's editory UI is valid.
*/
protected isTextValid_ = false;
/** Key down event data. */
private onKeyDownWrapper_: browserEvents.Data|null = null;
/** Key input event data. */
private onKeyInputWrapper_: browserEvents.Data|null = null;
/**
* Whether the field should consider the whole parent block to be its click
* target.
*/
fullBlockClickTarget_: boolean|null = false;
/** The workspace that this field belongs to. */
protected workspace_: WorkspaceSvg|null = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
*/
override SERIALIZABLE = true;
/** Mouse cursor style when over the hotspot that initiates the editor. */
override CURSOR = 'text';
override clickTarget_: AnyDuringMigration;
override value_: AnyDuringMigration;
override isDirty_: AnyDuringMigration;
/**
* @param opt_value The initial value of the field. Should cast to a string.
* Defaults to an empty string if null or undefined. Also accepts
* Field.SKIP_SETUP if you wish to skip setup (only used by subclasses
* that want to handle configuration and setting the field value after
* their own constructors have run).
* @param opt_validator A function that is called to validate changes to the
* field's value. Takes in a string & returns a validated string, or null
* to abort the change.
* @param opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/text-input#creation}
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|Sentinel, opt_validator?: FieldInputValidator<T>|null,
opt_config?: FieldInputConfig) {
super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(opt_value);
if (opt_validator) {
this.setValidator(opt_validator);
}
}
protected override configure_(config: FieldInputConfig) {
super.configure_(config);
if (config.spellcheck !== undefined) {
this.spellcheck_ = config.spellcheck;
}
}
/** @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 = block.inputList[i]; i++) {
for (let j = 0; input.fieldRow[j]; j++) {
nFields++;
}
if (input.connection) {
nConnections++;
}
}
// 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 && block.outputConnection && !nConnections;
} else {
this.fullBlockClickTarget_ = false;
}
if (this.fullBlockClickTarget_) {
this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot();
} else {
this.createBorderRect_();
}
this.createTextElement_();
}
/**
* Ensure that the input value casts to a valid string.
*
* @param opt_newValue The input value.
* @returns A valid string, or null if invalid.
*/
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
AnyDuringMigration {
if (opt_newValue === null || opt_newValue === undefined) {
return null;
}
return String(opt_newValue);
}
/**
* Called by setValue if the text input is not valid. If the field is
* currently being edited it reverts value of the field to the previous
* value while allowing the display text to be handled by the htmlInput_.
*
* @param _invalidValue The input value that was determined to be invalid.
* This is not used by the text input because its display value is stored
* on the htmlInput_.
*/
protected override doValueInvalid_(_invalidValue: AnyDuringMigration) {
if (this.isBeingEdited_) {
this.isDirty_ = true;
this.isTextValid_ = false;
const oldValue = this.value_;
// Revert value when the text becomes invalid.
this.value_ = this.htmlInput_!.getAttribute('data-untyped-default-value');
if (this.sourceBlock_ && eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this.sourceBlock_, 'field', this.name || null, oldValue,
this.value_));
}
}
}
/**
* Called by setValue if the text input is valid. Updates the value of the
* field, and updates the text of the field if it is not currently being
* edited (i.e. handled by the htmlInput_).
*
* @param newValue The value to be saved. The default validator guarantees
* that this is a string.
*/
protected override doValueUpdate_(newValue: AnyDuringMigration) {
this.isDirty_ = true;
this.isTextValid_ = true;
this.value_ = newValue;
}
/**
* Updates text field to match the colour/style of the block.
*
* @internal
*/
override applyColour() {
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);
}
}
/**
* Updates the colour of the htmlInput given the current validity of the
* field's value.
*/
protected override render_() {
super.render_();
// This logic is done in render_ rather than doValueInvalid_ or
// doValueUpdate_ so that the code is more centralized.
if (this.isBeingEdited_) {
this.resizeEditor_();
const htmlInput = this.htmlInput_ as HTMLElement;
if (!this.isTextValid_) {
dom.addClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, true);
} else {
dom.removeClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, false);
}
}
}
/**
* Set whether this field is spellchecked by the browser.
*
* @param check True if checked.
*/
setSpellcheck(check: boolean) {
if (check === this.spellcheck_) {
return;
}
this.spellcheck_ = check;
if (this.htmlInput_) {
// AnyDuringMigration because: Argument of type 'boolean' is not
// assignable to parameter of type 'string'.
this.htmlInput_.setAttribute(
'spellcheck', this.spellcheck_ as AnyDuringMigration);
}
}
/**
* Show an editor for the field.
* Shows the inline free-text editor on top of the text by default.
* Shows a prompt editor for mobile browsers if the modalInputs option is
* enabled.
*
* @param _opt_e Optional mouse event that triggered the field to open, or
* undefined if triggered programmatically.
* @param opt_quietInput True if editor should be created without focus.
* Defaults to false.
*/
protected override showEditor_(_opt_e?: Event, opt_quietInput?: boolean) {
this.workspace_ = (this.sourceBlock_ as BlockSvg).workspace;
const quietInput = opt_quietInput || false;
if (!quietInput && this.workspace_.options.modalInputs &&
(userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD)) {
this.showPromptEditor_();
} else {
this.showInlineEditor_(quietInput);
}
}
/**
* Create and show a text input editor that is a prompt (usually a popup).
* Mobile browsers may have issues with in-line textareas (focus and
* keyboards).
*/
private showPromptEditor_() {
dialog.prompt(
Msg['CHANGE_VALUE_TITLE'], this.getText(), (text: string|null) => {
// Text is null if user pressed cancel button.
if (text !== null) {
this.setValue(this.getValueFromEditorText_(text));
}
});
}
/**
* Create and show a text input editor that sits directly over the text input.
*
* @param quietInput True if editor should be created without focus.
*/
private showInlineEditor_(quietInput: boolean) {
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;
if (!quietInput) {
(this.htmlInput_ as HTMLElement).focus({
preventScroll: true,
});
this.htmlInput_.select();
}
}
/**
* Create the text input editor widget.
*
* @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();
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';
// AnyDuringMigration because: Argument of type 'boolean' is not assignable
// to parameter of type 'string'.
htmlInput.setAttribute(
'spellcheck', this.spellcheck_ as AnyDuringMigration);
const scale = this.workspace_!.getScale();
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt';
div!.style.fontSize = fontSize;
htmlInput.style.fontSize = fontSize;
let borderRadius = FieldInput.BORDERRADIUS * scale + 'px';
if (this.fullBlockClickTarget_) {
const bBox = this.getScaledBBox();
// Override border radius.
borderRadius = (bBox.bottom - bBox.top) / 2 + 'px';
// Pull stroke colour from the existing shadow block
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;
div!.style.transition = 'box-shadow 0.25s ease 0s';
if (this.getConstants()!.FIELD_TEXTINPUT_BOX_SHADOW) {
div!.style.boxShadow =
'rgba(255, 255, 255, 0.3) 0 0 0 ' + 4 * scale + 'px';
}
}
htmlInput.style.borderRadius = borderRadius;
div!.appendChild(htmlInput);
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
htmlInput.setAttribute('data-untyped-default-value', this.value_);
this.resizeEditor_();
this.bindInputEvents_(htmlInput);
return htmlInput;
}
/**
* Closes the editor, saves the results, and disposes of any events or
* DOM-references belonging to the editor.
*/
protected widgetDispose_() {
// Non-disposal related things that we do when the editor closes.
this.isBeingEdited_ = false;
this.isTextValid_ = true;
// Make sure the field's node matches the field's internal value.
this.forceRerender();
this.onFinishEditing_(this.value_);
eventUtils.setGroup(false);
// Actual disposal.
this.unbindInputEvents_();
const style = WidgetDiv.getDiv()!.style;
style.width = 'auto';
style.height = 'auto';
style.fontSize = '';
style.transition = '';
style.boxShadow = '';
this.htmlInput_ = null;
const clickTarget = this.getClickTarget_();
if (!clickTarget) throw new Error('A click target has not been set.');
dom.removeClass(clickTarget, 'editing');
}
/**
* A callback triggered when the user is done editing the field via the UI.
*
* @param _value The new value of the field.
*/
onFinishEditing_(_value: AnyDuringMigration) {}
// NOP by default.
// TODO(#2496): Support people passing a func into the field.
/**
* Bind handlers for user input on the text input field's editor.
*
* @param htmlInput The htmlInput to which event handlers will be bound.
*/
protected bindInputEvents_(htmlInput: HTMLElement) {
// Trap Enter without IME and Esc to hide.
this.onKeyDownWrapper_ = browserEvents.conditionalBind(
htmlInput, 'keydown', this, this.onHtmlInputKeyDown_);
// Resize after every input change.
this.onKeyInputWrapper_ = browserEvents.conditionalBind(
htmlInput, 'input', this, this.onHtmlInputChange_);
}
/** Unbind handlers for user input and workspace size changes. */
protected unbindInputEvents_() {
if (this.onKeyDownWrapper_) {
browserEvents.unbind(this.onKeyDownWrapper_);
this.onKeyDownWrapper_ = null;
}
if (this.onKeyInputWrapper_) {
browserEvents.unbind(this.onKeyInputWrapper_);
this.onKeyInputWrapper_ = null;
}
}
/**
* Handle key down to the editor.
*
* @param e Keyboard event.
*/
protected onHtmlInputKeyDown_(e: Event) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
if ((e as AnyDuringMigration).keyCode === KeyCodes.ENTER) {
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.ESC) {
this.setValue(
this.htmlInput_!.getAttribute('data-untyped-default-value'));
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.TAB) {
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
// AnyDuringMigration because: Property 'shiftKey' does not exist on type
// 'Event'. AnyDuringMigration because: Argument of type 'this' is not
// assignable to parameter of type 'Field'.
(this.sourceBlock_ as BlockSvg)
.tab(this as AnyDuringMigration, !(e as AnyDuringMigration).shiftKey);
e.preventDefault();
}
}
/**
* Handle a change to the editor.
*
* @param _e Keyboard event.
*/
private onHtmlInputChange_(_e: Event) {
this.setValue(this.getValueFromEditorText_(this.htmlInput_!.value));
}
/**
* Set the HTML input value and the field's internal value. The difference
* between this and `setValue` is that this also updates the HTML input
* value whilst editing.
*
* @param newValue New value.
*/
protected setEditorValue_(newValue: AnyDuringMigration) {
this.isDirty_ = true;
if (this.isBeingEdited_) {
// In the case this method is passed an invalid value, we still
// pass it through the transformation method `getEditorText` to deal
// with. Otherwise, the internal field's state will be inconsistent
// with what's shown to the user.
this.htmlInput_!.value = this.getEditorText_(newValue);
}
this.setValue(newValue);
}
/** 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';
div!.style.height = bBox.bottom - bBox.top + 'px';
// In RTL mode block fields and LTR input fields the left edge moves,
// whereas the right edge is fixed. Reposition the editor.
const x = block.RTL ? bBox.right - div!.offsetWidth : bBox.left;
const xy = new Coordinate(x, bBox.top);
div!.style.left = xy.x + 'px';
div!.style.top = xy.y + 'px';
}
/**
* Returns whether or not the field is tab navigable.
*
* @returns True if the field is tab navigable.
*/
override isTabNavigable(): boolean {
return true;
}
/**
* Use the `getText_` developer hook to override the field's text
* representation. When we're currently editing, return the current HTML value
* instead. Otherwise, return null which tells the field to use the default
* behaviour (which is a string cast of the field's value).
*
* @returns The HTML value if we're editing, otherwise null.
*/
protected override getText_(): string|null {
if (this.isBeingEdited_ && this.htmlInput_) {
// We are currently editing, return the HTML input value instead.
return this.htmlInput_.value;
}
return null;
}
/**
* Transform the provided value into a text to show in the HTML input.
* Override this method if the field's HTML input representation is different
* than the field's value. This should be coupled with an override of
* `getValueFromEditorText_`.
*
* @param value The value stored in this field.
* @returns The text to show on the HTML input.
*/
protected getEditorText_(value: AnyDuringMigration): string {
return String(value);
}
/**
* Transform the text received from the HTML input into a value to store
* in this field.
* Override this method if the field's HTML input representation is different
* than the field's value. This should be coupled with an override of
* `getEditorText_`.
*
* @param text Text received from the HTML input.
* @returns The value to store.
*/
protected getValueFromEditorText_(text: string): AnyDuringMigration {
return text;
}
}
/**
* Config options for the input field.
*/
export interface FieldInputConfig extends FieldConfig {
spellcheck?: boolean;
}

View File

@@ -14,18 +14,17 @@ 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 {Field, FieldConfig} from './field.js';
import * as fieldRegistry from './field_registry.js';
import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
/**
* Class for a non-editable, non-serializable text field.
*
* @alias Blockly.FieldLabel
*/
export class FieldLabel extends Field {
export class FieldLabel extends Field<string> {
/** The html class name to use for this field. */
private class_: string|null = null;
@@ -130,7 +129,7 @@ export class FieldLabel extends Field {
fieldRegistry.register('field_label', FieldLabel);
(FieldLabel.prototype as AnyDuringMigration).DEFAULT_VALUE = '';
FieldLabel.prototype.DEFAULT_VALUE = '';
// clang-format off
// Clang does not like the 'class' keyword being used as a property.

View File

@@ -14,11 +14,10 @@
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldLabelSerializable');
import {FieldLabelConfig, FieldLabelFromJsonConfig, FieldLabel} from './field_label.js';
import {FieldLabel, FieldLabelConfig, FieldLabelFromJsonConfig} from './field_label.js';
import * as fieldRegistry from './field_registry.js';
import * as parsing from './utils/parsing.js';
/**
* Class for a non-editable, serializable text field.
*

View File

@@ -15,7 +15,7 @@ goog.declareModuleId('Blockly.FieldMultilineInput');
import * as Css from './css.js';
import {Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {FieldTextInputConfig, FieldTextInput} from './field_textinput.js';
import {FieldTextInput, FieldTextInputConfig, FieldTextInputValidator} from './field_textinput.js';
import * as aria from './utils/aria.js';
import * as dom from './utils/dom.js';
import {KeyCodes} from './utils/keycodes.js';
@@ -25,6 +25,7 @@ import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
export type FieldMultilineInputValidator = FieldTextInputValidator;
/**
* Class for an editable text area field.
@@ -65,7 +66,7 @@ export class FieldMultilineInput extends FieldTextInput {
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|Sentinel, opt_validator?: Function,
opt_value?: string|Sentinel, opt_validator?: FieldMultilineInputValidator,
opt_config?: FieldMultilineInputConfig) {
super(Field.SKIP_SETUP);

View File

@@ -14,17 +14,18 @@ goog.declareModuleId('Blockly.FieldNumber');
import {Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {FieldTextInputConfig, FieldTextInput} from './field_textinput.js';
import {FieldInput, FieldInputConfig, FieldInputValidator} from './field_input.js';
import * as aria from './utils/aria.js';
import type {Sentinel} from './utils/sentinel.js';
export type FieldNumberValidator = FieldInputValidator<number>;
/**
* Class for an editable number field.
*
* @alias Blockly.FieldNumber
*/
export class FieldNumber extends FieldTextInput {
export class FieldNumber extends FieldInput<number> {
/** The minimum value this number field can contain. */
protected min_ = -Infinity;
@@ -46,6 +47,9 @@ export class FieldNumber extends FieldTextInput {
*/
override SERIALIZABLE = true;
/** Don't spellcheck numbers. Our validator does a better job. */
protected override spellcheck_ = false;
/**
* @param opt_value The initial value of the field. Should cast to a number.
* Defaults to 0. Also accepts Field.SKIP_SETUP if you wish to skip setup
@@ -68,7 +72,8 @@ export class FieldNumber extends FieldTextInput {
constructor(
opt_value?: string|number|Sentinel, opt_min?: string|number|null,
opt_max?: string|number|null, opt_precision?: string|number|null,
opt_validator?: Function|null, opt_config?: FieldNumberConfig) {
opt_validator?: FieldNumberValidator|null,
opt_config?: FieldNumberConfig) {
// Pass SENTINEL so that we can define properties before value validation.
super(Field.SKIP_SETUP);
@@ -310,7 +315,7 @@ export class FieldNumber extends FieldTextInput {
* @nocollapse
* @internal
*/
static override fromJson(options: FieldNumberFromJsonConfig): FieldNumber {
static fromJson(options: FieldNumberFromJsonConfig): FieldNumber {
// `this` might be a subclass of FieldNumber if that class doesn't override
// the static fromJson method.
return new this(
@@ -320,12 +325,12 @@ export class FieldNumber extends FieldTextInput {
fieldRegistry.register('field_number', FieldNumber);
(FieldNumber.prototype as AnyDuringMigration).DEFAULT_VALUE = 0;
FieldNumber.prototype.DEFAULT_VALUE = 0;
/**
* Config options for the number field.
*/
export interface FieldNumberConfig extends FieldTextInputConfig {
export interface FieldNumberConfig extends FieldInputConfig {
min?: number;
max?: number;
precision?: number;

View File

@@ -14,10 +14,13 @@
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.fieldRegistry');
import type {Field} from './field.js';
import type {IRegistrableField} from './interfaces/i_registrable_field.js';
import type {Field, FieldProto} from './field.js';
import * as registry from './registry.js';
interface RegistryOptions {
type: string;
[key: string]: unknown;
}
/**
* Registers a field type.
@@ -31,7 +34,7 @@ import * as registry from './registry.js';
* or the fieldClass is not an object containing a fromJson function.
* @alias Blockly.fieldRegistry.register
*/
export function register(type: string, fieldClass: IRegistrableField) {
export function register(type: string, fieldClass: FieldProto) {
registry.register(registry.Type.FIELD, type, fieldClass);
}
@@ -57,7 +60,7 @@ export function unregister(type: string) {
* @alias Blockly.fieldRegistry.fromJson
* @internal
*/
export function fromJson(options: AnyDuringMigration): Field|null {
export function fromJson<T>(options: RegistryOptions): Field<T>|null {
return TEST_ONLY.fromJsonInternal(options);
}
@@ -66,8 +69,8 @@ export function fromJson(options: AnyDuringMigration): Field|null {
*
* @param options
*/
function fromJsonInternal(options: AnyDuringMigration): Field|null {
const fieldObject = registry.getObject(registry.Type.FIELD, options['type']);
function fromJsonInternal<T>(options: RegistryOptions): Field<T>|null {
const fieldObject = registry.getObject(registry.Type.FIELD, options.type);
if (!fieldObject) {
console.warn(
'Blockly could not create a field of type ' + options['type'] +
@@ -78,7 +81,8 @@ function fromJsonInternal(options: AnyDuringMigration): Field|null {
} 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);
type fromJson = (options: {}) => Field<T>;
return (fieldObject as unknown as {fromJson: fromJson}).fromJson(options);
}
}

View File

@@ -15,78 +15,14 @@ goog.declareModuleId('Blockly.FieldTextInput');
// Unused import preserved for side-effects. Remove if unneeded.
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, UnattachedFieldError} from './field.js';
import {FieldInput, FieldInputConfig, FieldInputValidator} from './field_input.js';
import * as fieldRegistry from './field_registry.js';
import {Msg} from './msg.js';
import * as aria from './utils/aria.js';
import {Coordinate} from './utils/coordinate.js';
import {KeyCodes} from './utils/keycodes.js';
import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js';
export type FieldTextInputValidator = FieldInputValidator<string>;
/**
* Class for an editable text field.
*
* @alias Blockly.FieldTextInput
*/
export class FieldTextInput extends Field {
/**
* Pixel size of input border radius.
* Should match blocklyText's border-radius in CSS.
*/
static BORDERRADIUS = 4;
/** Allow browser to spellcheck this field. */
protected spellcheck_ = true;
/** The HTML input element. */
protected htmlInput_: HTMLInputElement|null = null;
/** True if the field's value is currently being edited via the UI. */
protected isBeingEdited_ = false;
/**
* True if the value currently displayed in the field's editory UI is valid.
*/
protected isTextValid_ = false;
/** Key down event data. */
private onKeyDownWrapper_: browserEvents.Data|null = null;
/** Key input event data. */
private onKeyInputWrapper_: browserEvents.Data|null = null;
/**
* Whether the field should consider the whole parent block to be its click
* target.
*/
fullBlockClickTarget_: boolean|null = false;
/** The workspace that this field belongs to. */
protected workspace_: WorkspaceSvg|null = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
*/
override SERIALIZABLE = true;
/** Mouse cursor style when over the hotspot that initiates the editor. */
override CURSOR = 'text';
override clickTarget_: AnyDuringMigration;
override value_: AnyDuringMigration;
override isDirty_: AnyDuringMigration;
export class FieldTextInput extends FieldInput<string> {
/**
* @param opt_value The initial value of the field. Should cast to a string.
* Defaults to an empty string if null or undefined. Also accepts
@@ -102,493 +38,9 @@ export class FieldTextInput extends Field {
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|Sentinel, opt_validator?: Function|null,
opt_config?: FieldTextInputConfig) {
super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(opt_value);
if (opt_validator) {
this.setValidator(opt_validator);
}
}
protected override configure_(config: FieldTextInputConfig) {
super.configure_(config);
if (config.spellcheck !== undefined) {
this.spellcheck_ = config.spellcheck;
}
}
/** @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 = block.inputList[i]; i++) {
for (let j = 0; input.fieldRow[j]; j++) {
nFields++;
}
if (input.connection) {
nConnections++;
}
}
// 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 && block.outputConnection && !nConnections;
} else {
this.fullBlockClickTarget_ = false;
}
if (this.fullBlockClickTarget_) {
this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot();
} else {
this.createBorderRect_();
}
this.createTextElement_();
}
/**
* Ensure that the input value casts to a valid string.
*
* @param opt_newValue The input value.
* @returns A valid string, or null if invalid.
*/
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
AnyDuringMigration {
if (opt_newValue === null || opt_newValue === undefined) {
return null;
}
return String(opt_newValue);
}
/**
* Called by setValue if the text input is not valid. If the field is
* currently being edited it reverts value of the field to the previous
* value while allowing the display text to be handled by the htmlInput_.
*
* @param _invalidValue The input value that was determined to be invalid.
* This is not used by the text input because its display value is stored
* on the htmlInput_.
*/
protected override doValueInvalid_(_invalidValue: AnyDuringMigration) {
if (this.isBeingEdited_) {
this.isTextValid_ = false;
const oldValue = this.value_;
// Revert value when the text becomes invalid.
this.value_ = this.htmlInput_!.getAttribute('data-untyped-default-value');
if (this.sourceBlock_ && eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this.sourceBlock_, 'field', this.name || null, oldValue,
this.value_));
}
}
}
/**
* Called by setValue if the text input is valid. Updates the value of the
* field, and updates the text of the field if it is not currently being
* edited (i.e. handled by the htmlInput_).
*
* @param newValue The value to be saved. The default validator guarantees
* that this is a string.
*/
protected override doValueUpdate_(newValue: AnyDuringMigration) {
this.isTextValid_ = true;
this.value_ = newValue;
if (!this.isBeingEdited_) {
// This should only occur if setValue is triggered programmatically.
this.isDirty_ = true;
}
}
/**
* Updates text field to match the colour/style of the block.
*
* @internal
*/
override applyColour() {
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);
}
}
/**
* Updates the colour of the htmlInput given the current validity of the
* field's value.
*/
protected override render_() {
super.render_();
// This logic is done in render_ rather than doValueInvalid_ or
// doValueUpdate_ so that the code is more centralized.
if (this.isBeingEdited_) {
this.resizeEditor_();
const htmlInput = this.htmlInput_ as HTMLElement;
if (!this.isTextValid_) {
dom.addClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, true);
} else {
dom.removeClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, false);
}
}
}
/**
* Set whether this field is spellchecked by the browser.
*
* @param check True if checked.
*/
setSpellcheck(check: boolean) {
if (check === this.spellcheck_) {
return;
}
this.spellcheck_ = check;
if (this.htmlInput_) {
// AnyDuringMigration because: Argument of type 'boolean' is not
// assignable to parameter of type 'string'.
this.htmlInput_.setAttribute(
'spellcheck', this.spellcheck_ as AnyDuringMigration);
}
}
/**
* Show the inline free-text editor on top of the text.
*
* @param _opt_e Optional mouse event that triggered the field to open, or
* undefined if triggered programmatically.
* @param opt_quietInput True if editor should be created without focus.
* Defaults to false.
*/
protected override showEditor_(_opt_e?: Event, opt_quietInput?: boolean) {
this.workspace_ = (this.sourceBlock_ as BlockSvg).workspace;
const quietInput = opt_quietInput || false;
if (!quietInput &&
(userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD)) {
this.showPromptEditor_();
} else {
this.showInlineEditor_(quietInput);
}
}
/**
* Create and show a text input editor that is a prompt (usually a popup).
* Mobile browsers have issues with in-line textareas (focus and keyboards).
*/
private showPromptEditor_() {
dialog.prompt(
Msg['CHANGE_VALUE_TITLE'], this.getText(), (text: string|null) => {
// Text is null if user pressed cancel button.
if (text !== null) {
this.setValue(this.getValueFromEditorText_(text));
}
});
}
/**
* Create and show a text input editor that sits directly over the text input.
*
* @param quietInput True if editor should be created without focus.
*/
private showInlineEditor_(quietInput: boolean) {
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;
if (!quietInput) {
(this.htmlInput_ as HTMLElement).focus({
preventScroll: true,
});
this.htmlInput_.select();
}
}
/**
* Create the text input editor widget.
*
* @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();
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';
// AnyDuringMigration because: Argument of type 'boolean' is not assignable
// to parameter of type 'string'.
htmlInput.setAttribute(
'spellcheck', this.spellcheck_ as AnyDuringMigration);
const scale = this.workspace_!.getScale();
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt';
div!.style.fontSize = fontSize;
htmlInput.style.fontSize = fontSize;
let borderRadius = FieldTextInput.BORDERRADIUS * scale + 'px';
if (this.fullBlockClickTarget_) {
const bBox = this.getScaledBBox();
// Override border radius.
borderRadius = (bBox.bottom - bBox.top) / 2 + 'px';
// Pull stroke colour from the existing shadow block
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;
div!.style.transition = 'box-shadow 0.25s ease 0s';
if (this.getConstants()!.FIELD_TEXTINPUT_BOX_SHADOW) {
div!.style.boxShadow =
'rgba(255, 255, 255, 0.3) 0 0 0 ' + 4 * scale + 'px';
}
}
htmlInput.style.borderRadius = borderRadius;
div!.appendChild(htmlInput);
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
htmlInput.setAttribute('data-untyped-default-value', this.value_);
htmlInput.setAttribute('data-old-value', '');
this.resizeEditor_();
this.bindInputEvents_(htmlInput);
return htmlInput;
}
/**
* Closes the editor, saves the results, and disposes of any events or
* DOM-references belonging to the editor.
*/
protected widgetDispose_() {
// Non-disposal related things that we do when the editor closes.
this.isBeingEdited_ = false;
this.isTextValid_ = true;
// Make sure the field's node matches the field's internal value.
this.forceRerender();
this.onFinishEditing_(this.value_);
eventUtils.setGroup(false);
// Actual disposal.
this.unbindInputEvents_();
const style = WidgetDiv.getDiv()!.style;
style.width = 'auto';
style.height = 'auto';
style.fontSize = '';
style.transition = '';
style.boxShadow = '';
this.htmlInput_ = null;
const clickTarget = this.getClickTarget_();
if (!clickTarget) throw new Error('A click target has not been set.');
dom.removeClass(clickTarget, 'editing');
}
/**
* A callback triggered when the user is done editing the field via the UI.
*
* @param _value The new value of the field.
*/
onFinishEditing_(_value: AnyDuringMigration) {}
// NOP by default.
// TODO(#2496): Support people passing a func into the field.
/**
* Bind handlers for user input on the text input field's editor.
*
* @param htmlInput The htmlInput to which event handlers will be bound.
*/
protected bindInputEvents_(htmlInput: HTMLElement) {
// Trap Enter without IME and Esc to hide.
this.onKeyDownWrapper_ = browserEvents.conditionalBind(
htmlInput, 'keydown', this, this.onHtmlInputKeyDown_);
// Resize after every input change.
this.onKeyInputWrapper_ = browserEvents.conditionalBind(
htmlInput, 'input', this, this.onHtmlInputChange_);
}
/** Unbind handlers for user input and workspace size changes. */
protected unbindInputEvents_() {
if (this.onKeyDownWrapper_) {
browserEvents.unbind(this.onKeyDownWrapper_);
this.onKeyDownWrapper_ = null;
}
if (this.onKeyInputWrapper_) {
browserEvents.unbind(this.onKeyInputWrapper_);
this.onKeyInputWrapper_ = null;
}
}
/**
* Handle key down to the editor.
*
* @param e Keyboard event.
*/
protected onHtmlInputKeyDown_(e: Event) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
if ((e as AnyDuringMigration).keyCode === KeyCodes.ENTER) {
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.ESC) {
this.setValue(
this.htmlInput_!.getAttribute('data-untyped-default-value'));
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.TAB) {
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
// AnyDuringMigration because: Property 'shiftKey' does not exist on type
// 'Event'. AnyDuringMigration because: Argument of type 'this' is not
// assignable to parameter of type 'Field'.
(this.sourceBlock_ as BlockSvg)
.tab(this as AnyDuringMigration, !(e as AnyDuringMigration).shiftKey);
e.preventDefault();
}
}
/**
* Handle a change to the editor.
*
* @param _e Keyboard event.
*/
private onHtmlInputChange_(_e: Event) {
const text = this.htmlInput_!.value;
if (text !== this.htmlInput_!.getAttribute('data-old-value')) {
this.htmlInput_!.setAttribute('data-old-value', text);
const value = this.getValueFromEditorText_(text);
this.setValue(value);
this.forceRerender();
this.resizeEditor_();
}
}
/**
* Set the HTML input value and the field's internal value. The difference
* between this and `setValue` is that this also updates the HTML input
* value whilst editing.
*
* @param newValue New value.
*/
protected setEditorValue_(newValue: AnyDuringMigration) {
this.isDirty_ = true;
if (this.isBeingEdited_) {
// In the case this method is passed an invalid value, we still
// pass it through the transformation method `getEditorText` to deal
// with. Otherwise, the internal field's state will be inconsistent
// with what's shown to the user.
this.htmlInput_!.value = this.getEditorText_(newValue);
}
this.setValue(newValue);
}
/** 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';
div!.style.height = bBox.bottom - bBox.top + 'px';
// In RTL mode block fields and LTR input fields the left edge moves,
// whereas the right edge is fixed. Reposition the editor.
const x = block.RTL ? bBox.right - div!.offsetWidth : bBox.left;
const xy = new Coordinate(x, bBox.top);
div!.style.left = xy.x + 'px';
div!.style.top = xy.y + 'px';
}
/**
* Returns whether or not the field is tab navigable.
*
* @returns True if the field is tab navigable.
*/
override isTabNavigable(): boolean {
return true;
}
/**
* Use the `getText_` developer hook to override the field's text
* representation. When we're currently editing, return the current HTML value
* instead. Otherwise, return null which tells the field to use the default
* behaviour (which is a string cast of the field's value).
*
* @returns The HTML value if we're editing, otherwise null.
*/
protected override getText_(): string|null {
if (this.isBeingEdited_ && this.htmlInput_) {
// We are currently editing, return the HTML input value instead.
return this.htmlInput_.value;
}
return null;
}
/**
* Transform the provided value into a text to show in the HTML input.
* Override this method if the field's HTML input representation is different
* than the field's value. This should be coupled with an override of
* `getValueFromEditorText_`.
*
* @param value The value stored in this field.
* @returns The text to show on the HTML input.
*/
protected getEditorText_(value: AnyDuringMigration): string {
return String(value);
}
/**
* Transform the text received from the HTML input into a value to store
* in this field.
* Override this method if the field's HTML input representation is different
* than the field's value. This should be coupled with an override of
* `getEditorText_`.
*
* @param text Text received from the HTML input.
* @returns The value to store.
*/
protected getValueFromEditorText_(text: string): AnyDuringMigration {
return text;
opt_value?: string|Sentinel, opt_validator?: FieldTextInputValidator|null,
opt_config?: FieldInputConfig) {
super(opt_value, opt_validator, opt_config);
}
/**
@@ -610,18 +62,13 @@ export class FieldTextInput extends Field {
fieldRegistry.register('field_input', FieldTextInput);
(FieldTextInput.prototype as AnyDuringMigration).DEFAULT_VALUE = '';
/**
* Config options for the text input field.
*/
export interface FieldTextInputConfig extends FieldConfig {
spellcheck?: boolean;
}
FieldTextInput.prototype.DEFAULT_VALUE = '';
/**
* fromJson config options for the text input field.
*/
export interface FieldTextInputFromJsonConfig extends FieldTextInputConfig {
export interface FieldTextInputFromJsonConfig extends FieldInputConfig {
text?: string;
}
export {FieldInputConfig as FieldTextInputConfig};

View File

@@ -17,7 +17,7 @@ import './events/events_block_change.js';
import type {Block} from './block.js';
import {Field, FieldConfig, UnattachedFieldError} from './field.js';
import {FieldDropdown, MenuGenerator, MenuOption} from './field_dropdown.js';
import {FieldDropdown, FieldDropdownValidator, 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';
@@ -30,6 +30,7 @@ import {VariableModel} from './variable_model.js';
import * as Variables from './variables.js';
import * as Xml from './xml.js';
export type FieldVariableValidator = FieldDropdownValidator;
/**
* Class for a variable's dropdown field.
@@ -79,7 +80,7 @@ export class FieldVariable extends FieldDropdown {
* for a list of properties this parameter supports.
*/
constructor(
varName: string|null|Sentinel, opt_validator?: Function,
varName: string|null|Sentinel, opt_validator?: FieldVariableValidator,
opt_variableTypes?: string[], opt_defaultType?: string,
opt_config?: FieldVariableConfig) {
super(Field.SKIP_SETUP);

View File

@@ -371,7 +371,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
Array.prototype.push.apply(
this.eventWrappers_,
browserEvents.conditionalBind(
(this.svgBackground_ as SVGPathElement), 'mousedown', this,
(this.svgBackground_ as SVGPathElement), 'pointerdown', this,
this.onMouseDown_));
// A flyout connected to a workspace doesn't have its own current gesture.
@@ -450,7 +450,6 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* Get the workspace inside the flyout.
*
* @returns The workspace inside the flyout.
* @internal
*/
getWorkspace(): WorkspaceSvg {
return this.workspace_;
@@ -615,7 +614,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
}
this.listeners_.push(browserEvents.conditionalBind(
(this.svgBackground_ as SVGPathElement), 'mouseover', this,
(this.svgBackground_ as SVGPathElement), 'pointerover', this,
deselectAll));
if (this.horizontalLayout) {
@@ -912,27 +911,27 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
protected addBlockListeners_(
root: SVGElement, block: BlockSvg, rect: SVGElement) {
this.listeners_.push(browserEvents.conditionalBind(
root, 'mousedown', null, this.blockMouseDown_(block)));
root, 'pointerdown', null, this.blockMouseDown_(block)));
this.listeners_.push(browserEvents.conditionalBind(
rect, 'mousedown', null, this.blockMouseDown_(block)));
rect, 'pointerdown', null, this.blockMouseDown_(block)));
this.listeners_.push(
browserEvents.bind(root, 'mouseenter', block, block.addSelect));
browserEvents.bind(root, 'pointerenter', block, block.addSelect));
this.listeners_.push(
browserEvents.bind(root, 'mouseleave', block, block.removeSelect));
browserEvents.bind(root, 'pointerleave', block, block.removeSelect));
this.listeners_.push(
browserEvents.bind(rect, 'mouseenter', block, block.addSelect));
browserEvents.bind(rect, 'pointerenter', block, block.addSelect));
this.listeners_.push(
browserEvents.bind(rect, 'mouseleave', block, block.removeSelect));
browserEvents.bind(rect, 'pointerleave', block, block.removeSelect));
}
/**
* Handle a mouse-down on an SVG block in a non-closing flyout.
* Handle a pointerdown on an SVG block in a non-closing flyout.
*
* @param block The flyout block to copy.
* @returns Function to call when block is clicked.
*/
private blockMouseDown_(block: BlockSvg): Function {
return (e: MouseEvent) => {
return (e: PointerEvent) => {
const gesture = this.targetWorkspace.getGesture(e);
if (gesture) {
gesture.setStartBlock(block);
@@ -942,11 +941,11 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
}
/**
* Mouse down on the flyout background. Start a vertical scroll drag.
* Pointer down on the flyout background. Start a vertical scroll drag.
*
* @param e Mouse down event.
* @param e Pointer down event.
*/
private onMouseDown_(e: MouseEvent) {
private onMouseDown_(e: PointerEvent) {
const gesture = this.targetWorkspace.getGesture(e);
if (gesture) {
gesture.handleFlyoutStart(e, this);
@@ -1027,7 +1026,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
// Clicking on a flyout button or label is a lot like clicking on the
// flyout background.
this.listeners_.push(browserEvents.conditionalBind(
buttonSvg, 'mousedown', this, this.onMouseDown_));
buttonSvg, 'pointerdown', this, this.onMouseDown_));
this.buttons_.push(button);
}

View File

@@ -170,7 +170,8 @@ export class FlyoutButton {
// AnyDuringMigration because: Argument of type 'SVGGElement | null' is not
// assignable to parameter of type 'EventTarget'.
this.onMouseUpWrapper_ = browserEvents.conditionalBind(
this.svgGroup_ as AnyDuringMigration, 'mouseup', this, this.onMouseUp_);
this.svgGroup_ as AnyDuringMigration, 'pointerup', this,
this.onMouseUp_);
return this.svgGroup_!;
}
@@ -244,9 +245,9 @@ export class FlyoutButton {
/**
* Do something when the button is clicked.
*
* @param e Mouse up event.
* @param e Pointer up event.
*/
private onMouseUp_(e: Event) {
private onMouseUp_(e: PointerEvent) {
const gesture = this.targetWorkspace.getGesture(e);
if (gesture) {
gesture.cancel();

View File

@@ -5,8 +5,8 @@
*/
/**
* The class representing an in-progress gesture, usually a drag
* or a tap.
* The class representing an in-progress gesture, e.g. a drag,
* tap, or pinch to zoom.
*
* @class
*/
@@ -22,6 +22,7 @@ import * as browserEvents from './browser_events.js';
import {BubbleDragger} from './bubble_dragger.js';
import * as common from './common.js';
import {config} from './config.js';
import * as dropDownDiv from './dropdowndiv.js';
import * as eventUtils from './events/utils.js';
import type {Field} from './field.js';
import type {IBlockDragger} from './interfaces/i_block_dragger.js';
@@ -38,10 +39,16 @@ import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Note: In this file "start" refers to touchstart, mousedown, and pointerstart
* events. "End" refers to touchend, mouseup, and pointerend events.
* Note: In this file "start" refers to pointerdown
* events. "End" refers to pointerup events.
*/
// TODO: Consider touchcancel/pointercancel.
/** A multiplier used to convert the gesture scale to a zoom in delta. */
const ZOOM_IN_MULTIPLIER = 5;
/** A multiplier used to convert the gesture scale to a zoom out delta. */
const ZOOM_OUT_MULTIPLIER = 6;
/**
* Class for one gesture.
*
@@ -49,8 +56,8 @@ import type {WorkspaceSvg} from './workspace_svg.js';
*/
export class Gesture {
/**
* The position of the mouse when the gesture started. Units are CSS
* pixels, with (0, 0) at the top left of the browser window (mouseEvent
* The position of the pointer when the gesture started. Units are CSS
* pixels, with (0, 0) at the top left of the browser window (pointer event
* clientX/Y).
*/
private mouseDownXY_ = new Coordinate(0, 0);
@@ -97,13 +104,13 @@ export class Gesture {
private hasExceededDragRadius_ = false;
/**
* A handle to use to unbind a mouse move listener at the end of a drag.
* A handle to use to unbind a pointermove listener at the end of a drag.
* Opaque data returned from Blockly.bindEventWithChecks_.
*/
protected onMoveWrapper_: browserEvents.Data|null = null;
/**
* A handle to use to unbind a mouse up listener at the end of a drag.
* A handle to use to unbind a pointerup listener at the end of a drag.
* Opaque data returned from Blockly.bindEventWithChecks_.
*/
protected onUpWrapper_: browserEvents.Data|null = null;
@@ -134,18 +141,54 @@ export class Gesture {
private healStack_: boolean;
/** The event that most recently updated this gesture. */
private mostRecentEvent_: Event;
private mostRecentEvent_: PointerEvent;
/** Boolean for whether or not this gesture is a multi-touch gesture. */
private isMultiTouch_ = false;
/** A map of cached points used for tracking multi-touch gestures. */
private cachedPoints = new Map<string, Coordinate|null>();
/**
* This is the ratio between the starting distance between the touch points
* and the most recent distance between the touch points.
* Scales between 0 and 1 mean the most recent zoom was a zoom out.
* Scales above 1.0 mean the most recent zoom was a zoom in.
*/
private previousScale_ = 0;
/** The starting distance between two touch points. */
private startDistance_ = 0;
/**
* A handle to use to unbind the second pointerdown listener
* at the end of a drag.
* Opaque data returned from Blockly.bindEventWithChecks_.
*/
private onStartWrapper_: browserEvents.Data|null = null;
/** Boolean for whether or not the workspace supports pinch-zoom. */
private isPinchZoomEnabled_: boolean|null = null;
/**
* The owner of the dropdownDiv when this gesture first starts.
* Needed because we'll close the dropdown before fields get to
* act on their events, and some fields care about who owns
* the dropdown.
*/
currentDropdownOwner: Field|null = null;
/**
* @param e The event that kicked off this gesture.
* @param creatorWorkspace The workspace that created this gesture and has a
* reference to it.
*/
constructor(e: Event, private readonly creatorWorkspace: WorkspaceSvg) {
constructor(
e: PointerEvent, private readonly creatorWorkspace: WorkspaceSvg) {
this.mostRecentEvent_ = e;
/**
* How far the mouse has moved during this drag, in pixel units.
* How far the pointer has moved during this drag, in pixel units.
* (0, 0) is at this.mouseDownXY_.
*/
this.currentDragDeltaXY_ = new Coordinate(0, 0);
@@ -181,19 +224,19 @@ export class Gesture {
if (this.workspaceDragger_) {
this.workspaceDragger_.dispose();
}
if (this.onStartWrapper_) {
browserEvents.unbind(this.onStartWrapper_);
}
}
/**
* Update internal state based on an event.
*
* @param e The most recent mouse or touch event.
* @param e The most recent pointer event.
*/
private updateFromEvent_(e: Event) {
// AnyDuringMigration because: Property 'clientY' does not exist on type
// 'Event'. AnyDuringMigration because: Property 'clientX' does not exist
// on type 'Event'.
const currentXY = new Coordinate(
(e as AnyDuringMigration).clientX, (e as AnyDuringMigration).clientY);
private updateFromEvent_(e: PointerEvent) {
const currentXY = new Coordinate(e.clientX, e.clientY);
const changed = this.updateDragDelta_(currentXY);
// Exceeded the drag radius for the first time.
if (changed) {
@@ -204,9 +247,10 @@ export class Gesture {
}
/**
* DO MATH to set currentDragDeltaXY_ based on the most recent mouse position.
* DO MATH to set currentDragDeltaXY_ based on the most recent pointer
* position.
*
* @param currentXY The most recent mouse/pointer position, in pixel units,
* @param currentXY The most recent pointer position, in pixel units,
* with (0, 0) at the window's top left corner.
* @returns True if the drag just exceeded the drag radius for the first time.
*/
@@ -230,7 +274,7 @@ export class Gesture {
/**
* Update this gesture to record whether a block is being dragged from the
* flyout.
* This function should be called on a mouse/touch move event the first time
* This function should be called on a pointermove event the first time
* the drag radius is exceeded. It should be called no more than once per
* gesture. If a block should be dragged from the flyout this function creates
* the new block on the main workspace and updates targetBlock_ and
@@ -267,7 +311,7 @@ export class Gesture {
/**
* Update this gesture to record whether a bubble is being dragged.
* This function should be called on a mouse/touch move event the first time
* This function should be called on a pointermove event the first time
* the drag radius is exceeded. It should be called no more than once per
* gesture. If a bubble should be dragged this function creates the necessary
* BubbleDragger and starts the drag.
@@ -288,7 +332,7 @@ export class Gesture {
* from the flyout or in the workspace, create the necessary BlockDragger and
* start the drag.
*
* This function should be called on a mouse/touch move event the first time
* This function should be called on a pointermove event the first time
* the drag radius is exceeded. It should be called no more than once per
* gesture. If a block should be dragged, either from the flyout or in the
* workspace, this function creates the necessary BlockDragger and starts the
@@ -316,7 +360,7 @@ export class Gesture {
* Check whether to start a workspace drag. If a workspace is being dragged,
* create the necessary WorkspaceDragger and start the drag.
*
* This function should be called on a mouse/touch move event the first time
* This function should be called on a pointermove event the first time
* the drag radius is exceeded. It should be called no more than once per
* gesture. If a workspace is being dragged this function creates the
* necessary WorkspaceDragger and starts the drag.
@@ -340,7 +384,7 @@ export class Gesture {
/**
* Update this gesture to record whether anything is being dragged.
* This function should be called on a mouse/touch move event the first time
* This function should be called on a pointermove event the first time
* the drag radius is exceeded. It should be called no more than once per
* gesture.
*/
@@ -398,23 +442,25 @@ export class Gesture {
/**
* Start a gesture: update the workspace to indicate that a gesture is in
* progress and bind mousemove and mouseup handlers.
* progress and bind pointermove and pointerup handlers.
*
* @param e A mouse down or touch start event.
* @param e A pointerdown event.
* @internal
*/
doStart(e: MouseEvent) {
doStart(e: PointerEvent) {
if (!this.startWorkspace_) {
throw new Error(
'Cannot start the touch gesture becauase the start ' +
'workspace is undefined');
}
this.isPinchZoomEnabled_ = this.startWorkspace_.options.zoomOptions &&
this.startWorkspace_.options.zoomOptions.pinch;
if (browserEvents.isTargetInput(e)) {
this.cancel();
return;
}
if (!this.startWorkspace_) {
throw new Error(
'Cannot start the gesture because the start ' +
'workspace is undefined');
}
this.hasStarted_ = true;
blockAnimations.disconnectUiStop();
@@ -425,6 +471,9 @@ export class Gesture {
// dragged, the block was moved, the parent workspace zoomed, etc.
this.startWorkspace_.resize();
}
// Keep track of which field owns the dropdown before we close it.
this.currentDropdownOwner = dropDownDiv.getOwner();
// Hide chaff also hides the flyout, so don't do it if the click is in a
// flyout.
this.startWorkspace_.hideChaff(!!this.flyout_);
@@ -443,106 +492,252 @@ export class Gesture {
return;
}
// TODO(#6097): Make types accurate, possibly by refactoring touch handling.
const typelessEvent = e as AnyDuringMigration;
if ((e.type.toLowerCase() === 'touchstart' ||
e.type.toLowerCase() === 'pointerdown') &&
typelessEvent.pointerType !== 'mouse') {
Touch.longStart(typelessEvent, this);
if (e.type.toLowerCase() === 'pointerdown' && e.pointerType !== 'mouse') {
Touch.longStart(e, this);
}
// AnyDuringMigration because: Property 'clientY' does not exist on type
// 'Event'. AnyDuringMigration because: Property 'clientX' does not exist
// on type 'Event'.
this.mouseDownXY_ = new Coordinate(
(e as AnyDuringMigration).clientX, (e as AnyDuringMigration).clientY);
// AnyDuringMigration because: Property 'metaKey' does not exist on type
// 'Event'. AnyDuringMigration because: Property 'ctrlKey' does not exist
// on type 'Event'. AnyDuringMigration because: Property 'altKey' does not
// exist on type 'Event'.
this.healStack_ = (e as AnyDuringMigration).altKey ||
(e as AnyDuringMigration).ctrlKey || (e as AnyDuringMigration).metaKey;
this.mouseDownXY_ = new Coordinate(e.clientX, e.clientY);
this.healStack_ = e.altKey || e.ctrlKey || e.metaKey;
this.bindMouseEvents(e);
if (!this.isEnding_) {
this.handleTouchStart(e);
}
}
/**
* Bind gesture events.
*
* @param e A mouse down or touch start event.
* @param e A pointerdown event.
* @internal
*/
bindMouseEvents(e: Event) {
bindMouseEvents(e: PointerEvent) {
this.onStartWrapper_ = browserEvents.conditionalBind(
document, 'pointerdown', null, this.handleStart.bind(this),
/* opt_noCaptureIdentifier */ true);
this.onMoveWrapper_ = browserEvents.conditionalBind(
document, 'mousemove', null, this.handleMove.bind(this));
document, 'pointermove', null, this.handleMove.bind(this),
/* opt_noCaptureIdentifier */ true);
this.onUpWrapper_ = browserEvents.conditionalBind(
document, 'mouseup', null, this.handleUp.bind(this));
document, 'pointerup', null, this.handleUp.bind(this),
/* opt_noCaptureIdentifier */ true);
e.preventDefault();
e.stopPropagation();
}
/**
* Handle a mouse move or touch move event.
* Handle a pointerdown event.
*
* @param e A mouse move or touch move event.
* @param e A pointerdown event.
* @internal
*/
handleMove(e: Event) {
this.updateFromEvent_(e);
if (this.workspaceDragger_) {
this.workspaceDragger_.drag(this.currentDragDeltaXY_);
} else if (this.blockDragger_) {
this.blockDragger_.drag(this.mostRecentEvent_, this.currentDragDeltaXY_);
} else if (this.bubbleDragger_) {
this.bubbleDragger_.dragBubble(
this.mostRecentEvent_, this.currentDragDeltaXY_);
}
e.preventDefault();
e.stopPropagation();
}
/**
* Handle a mouse up or touch end event.
*
* @param e A mouse up or touch end event.
* @internal
*/
handleUp(e: Event) {
this.updateFromEvent_(e);
Touch.longStop();
if (this.isEnding_) {
console.log('Trying to end a gesture recursively.');
handleStart(e: PointerEvent) {
if (this.isDragging()) {
// A drag has already started, so this can no longer be a pinch-zoom.
return;
}
this.isEnding_ = true;
// The ordering of these checks is important: drags have higher priority
// than clicks. Fields have higher priority than blocks; blocks have higher
// priority than workspaces.
// The ordering within drags does not matter, because the three types of
// dragging are exclusive.
if (this.bubbleDragger_) {
this.bubbleDragger_.endBubbleDrag(e, this.currentDragDeltaXY_);
} else if (this.blockDragger_) {
this.blockDragger_.endDrag(e, this.currentDragDeltaXY_);
} else if (this.workspaceDragger_) {
this.workspaceDragger_.endDrag(this.currentDragDeltaXY_);
} else if (this.isBubbleClick_()) {
// Bubbles are in front of all fields and blocks.
this.doBubbleClick_();
} else if (this.isFieldClick_()) {
this.doFieldClick_();
} else if (this.isBlockClick_()) {
this.doBlockClick_();
} else if (this.isWorkspaceClick_()) {
this.doWorkspaceClick_(e);
this.handleTouchStart(e);
if (this.isMultiTouch()) {
Touch.longStop();
}
}
/**
* Handle a pointermove event.
*
* @param e A pointermove event.
* @internal
*/
handleMove(e: PointerEvent) {
if ((this.isDragging() && Touch.shouldHandleEvent(e)) ||
!this.isMultiTouch()) {
this.updateFromEvent_(e);
if (this.workspaceDragger_) {
this.workspaceDragger_.drag(this.currentDragDeltaXY_);
} else if (this.blockDragger_) {
this.blockDragger_.drag(
this.mostRecentEvent_, this.currentDragDeltaXY_);
} else if (this.bubbleDragger_) {
this.bubbleDragger_.dragBubble(
this.mostRecentEvent_, this.currentDragDeltaXY_);
}
e.preventDefault();
e.stopPropagation();
} else if (this.isMultiTouch()) {
this.handleTouchMove(e);
Touch.longStop();
}
}
/**
* Handle a pointerup event.
*
* @param e A pointerup event.
* @internal
*/
handleUp(e: PointerEvent) {
if (!this.isDragging()) {
this.handleTouchEnd(e);
}
if (!this.isMultiTouch() || this.isDragging()) {
if (!Touch.shouldHandleEvent(e)) {
return;
}
this.updateFromEvent_(e);
Touch.longStop();
if (this.isEnding_) {
console.log('Trying to end a gesture recursively.');
return;
}
this.isEnding_ = true;
// The ordering of these checks is important: drags have higher priority
// than clicks. Fields have higher priority than blocks; blocks have
// higher priority than workspaces. The ordering within drags does not
// matter, because the three types of dragging are exclusive.
if (this.bubbleDragger_) {
this.bubbleDragger_.endBubbleDrag(e, this.currentDragDeltaXY_);
} else if (this.blockDragger_) {
this.blockDragger_.endDrag(e, this.currentDragDeltaXY_);
} else if (this.workspaceDragger_) {
this.workspaceDragger_.endDrag(this.currentDragDeltaXY_);
} else if (this.isBubbleClick_()) {
// Bubbles are in front of all fields and blocks.
this.doBubbleClick_();
} else if (this.isFieldClick_()) {
this.doFieldClick_();
} else if (this.isBlockClick_()) {
this.doBlockClick_();
} else if (this.isWorkspaceClick_()) {
this.doWorkspaceClick_(e);
}
e.preventDefault();
e.stopPropagation();
this.dispose();
} else {
e.preventDefault();
e.stopPropagation();
this.dispose();
}
}
/**
* Handle a pointerdown event and keep track of current
* pointers.
*
* @param e A pointerdown event.
* @internal
*/
handleTouchStart(e: PointerEvent) {
const pointerId = Touch.getTouchIdentifierFromEvent(e);
// store the pointerId in the current list of pointers
this.cachedPoints.set(pointerId, this.getTouchPoint(e));
const pointers = Array.from(this.cachedPoints.keys());
// If two pointers are down, store info
if (pointers.length === 2) {
const point0 = (this.cachedPoints.get(pointers[0]))!;
const point1 = (this.cachedPoints.get(pointers[1]))!;
this.startDistance_ = Coordinate.distance(point0, point1);
this.isMultiTouch_ = true;
e.preventDefault();
}
}
/**
* Handle a pointermove event and zoom in/out if two pointers
* are on the screen.
*
* @param e A pointermove event.
* @internal
*/
handleTouchMove(e: PointerEvent) {
const pointerId = Touch.getTouchIdentifierFromEvent(e);
// Update the cache
this.cachedPoints.set(pointerId, this.getTouchPoint(e));
if (this.isPinchZoomEnabled_ && this.cachedPoints.size === 2) {
this.handlePinch_(e);
} else {
this.handleMove(e);
}
}
/**
* Handle pinch zoom gesture.
*
* @param e A pointermove event.
*/
private handlePinch_(e: PointerEvent) {
const pointers = Array.from(this.cachedPoints.keys());
// Calculate the distance between the two pointers
const point0 = (this.cachedPoints.get(pointers[0]))!;
const point1 = (this.cachedPoints.get(pointers[1]))!;
const moveDistance = Coordinate.distance(point0, point1);
const scale = moveDistance / this.startDistance_;
if (this.previousScale_ > 0 && this.previousScale_ < Infinity) {
const gestureScale = scale - this.previousScale_;
const delta = gestureScale > 0 ? gestureScale * ZOOM_IN_MULTIPLIER :
gestureScale * ZOOM_OUT_MULTIPLIER;
if (!this.startWorkspace_) {
throw new Error(
'Cannot handle a pinch because the start workspace ' +
'is undefined');
}
const workspace = this.startWorkspace_;
const position = browserEvents.mouseToSvg(
e, workspace.getParentSvg(), workspace.getInverseScreenCTM());
workspace.zoom(position.x, position.y, delta);
}
this.previousScale_ = scale;
e.preventDefault();
e.stopPropagation();
}
this.dispose();
/**
* Handle a pointerup event and end the gesture.
*
* @param e A pointerup event.
* @internal
*/
handleTouchEnd(e: PointerEvent) {
const pointerId = Touch.getTouchIdentifierFromEvent(e);
if (this.cachedPoints.has(pointerId)) {
this.cachedPoints.delete(pointerId);
}
if (this.cachedPoints.size < 2) {
this.cachedPoints.clear();
this.previousScale_ = 0;
}
}
/**
* Helper function returning the current touch point coordinate.
*
* @param e A pointer event.
* @returns The current touch point coordinate
* @internal
*/
getTouchPoint(e: PointerEvent): Coordinate|null {
if (!this.startWorkspace_) {
return null;
}
return new Coordinate(e.pageX, e.pageY);
}
/**
* Whether this gesture is part of a multi-touch gesture.
*
* @returns Whether this gesture is part of a multi-touch gesture.
* @internal
*/
isMultiTouch(): boolean {
return this.isMultiTouch_;
}
/**
@@ -574,10 +769,10 @@ export class Gesture {
/**
* Handle a real or faked right-click event by showing a context menu.
*
* @param e A mouse move or touch move event.
* @param e A pointerdown event.
* @internal
*/
handleRightClick(e: Event) {
handleRightClick(e: PointerEvent) {
if (this.targetBlock_) {
this.bringBlockToFront_();
this.targetBlock_.workspace.hideChaff(!!this.flyout_);
@@ -597,13 +792,13 @@ export class Gesture {
}
/**
* Handle a mousedown/touchstart event on a workspace.
* Handle a pointerdown event on a workspace.
*
* @param e A mouse down or touch start event.
* @param e A pointerdown event.
* @param ws The workspace the event hit.
* @internal
*/
handleWsStart(e: MouseEvent, ws: WorkspaceSvg) {
handleWsStart(e: PointerEvent, ws: WorkspaceSvg) {
if (this.hasStarted_) {
throw Error(
'Tried to call gesture.handleWsStart, ' +
@@ -625,13 +820,13 @@ export class Gesture {
}
/**
* Handle a mousedown/touchstart event on a flyout.
* Handle a pointerdown event on a flyout.
*
* @param e A mouse down or touch start event.
* @param e A pointerdown event.
* @param flyout The flyout the event hit.
* @internal
*/
handleFlyoutStart(e: MouseEvent, flyout: IFlyout) {
handleFlyoutStart(e: PointerEvent, flyout: IFlyout) {
if (this.hasStarted_) {
throw Error(
'Tried to call gesture.handleFlyoutStart, ' +
@@ -642,13 +837,13 @@ export class Gesture {
}
/**
* Handle a mousedown/touchstart event on a block.
* Handle a pointerdown event on a block.
*
* @param e A mouse down or touch start event.
* @param e A pointerdown event.
* @param block The block the event hit.
* @internal
*/
handleBlockStart(e: Event, block: BlockSvg) {
handleBlockStart(e: PointerEvent, block: BlockSvg) {
if (this.hasStarted_) {
throw Error(
'Tried to call gesture.handleBlockStart, ' +
@@ -659,13 +854,13 @@ export class Gesture {
}
/**
* Handle a mousedown/touchstart event on a bubble.
* Handle a pointerdown event on a bubble.
*
* @param e A mouse down or touch start event.
* @param e A pointerdown event.
* @param bubble The bubble the event hit.
* @internal
*/
handleBubbleStart(e: Event, bubble: IBubble) {
handleBubbleStart(e: PointerEvent, bubble: IBubble) {
if (this.hasStarted_) {
throw Error(
'Tried to call gesture.handleBubbleStart, ' +
@@ -695,7 +890,13 @@ export class Gesture {
'Cannot do a field click because the start field is ' +
'undefined');
}
this.startField_.showEditor(this.mostRecentEvent_);
// Only show the editor if the field's editor wasn't already open
// right before this gesture started.
const dropdownAlreadyOpen = this.currentDropdownOwner === this.startField_;
if (!dropdownAlreadyOpen) {
this.startField_.showEditor(this.mostRecentEvent_);
}
this.bringBlockToFront_();
}
@@ -734,9 +935,9 @@ export class Gesture {
* Execute a workspace click. When in accessibility mode shift clicking will
* move the cursor.
*
* @param _e A mouse up or touch end event.
* @param _e A pointerup event.
*/
private doWorkspaceClick_(_e: Event) {
private doWorkspaceClick_(_e: PointerEvent) {
const ws = this.creatorWorkspace;
if (common.getSelected()) {
common.getSelected()!.unselect();
@@ -760,7 +961,7 @@ export class Gesture {
}
}
/* Begin functions for populating a gesture at mouse down. */
/* Begin functions for populating a gesture at pointerdown. */
/**
* Record the field that a gesture started on.
@@ -849,14 +1050,14 @@ export class Gesture {
}
}
/* End functions for populating a gesture at mouse down. */
/* End functions for populating a gesture at pointerdown. */
/* Begin helper functions defining types of clicks. Any developer wanting
* to change the definition of a click should modify only this code. */
/**
* Whether this gesture is a click on a bubble. This should only be called
* when ending a gesture (mouse up, touch end).
* when ending a gesture (pointerup).
*
* @returns Whether this gesture was a click on a bubble.
*/
@@ -868,7 +1069,7 @@ export class Gesture {
/**
* Whether this gesture is a click on a block. This should only be called
* when ending a gesture (mouse up, touch end).
* when ending a gesture (pointerup).
*
* @returns Whether this gesture was a click on a block.
*/
@@ -882,7 +1083,7 @@ export class Gesture {
/**
* Whether this gesture is a click on a field. This should only be called
* when ending a gesture (mouse up, touch end).
* when ending a gesture (pointerup).
*
* @returns Whether this gesture was a click on a field.
*/
@@ -895,7 +1096,7 @@ export class Gesture {
/**
* Whether this gesture is a click on a workspace. This should only be called
* when ending a gesture (mouse up, touch end).
* when ending a gesture (pointerup).
*
* @returns Whether this gesture was a click on a workspace.
*/
@@ -921,9 +1122,9 @@ export class Gesture {
}
/**
* Whether this gesture has already been started. In theory every mouse down
* has a corresponding mouse up, but in reality it is possible to lose a
* mouse up, leaving an in-process gesture hanging.
* Whether this gesture has already been started. In theory every pointerdown
* has a corresponding pointerup, but in reality it is possible to lose a
* pointerup, leaving an in-process gesture hanging.
*
* @returns Whether this gesture was a click on a workspace.
* @internal

View File

@@ -75,7 +75,7 @@ export abstract class Icon {
this.getBlock().getSvgRoot().appendChild(this.iconGroup_);
browserEvents.conditionalBind(
this.iconGroup_, 'mouseup', this, this.iconClick_);
this.iconGroup_, 'pointerup', this, this.iconClick_);
this.updateEditable();
}
@@ -104,7 +104,7 @@ export abstract class Icon {
*
* @param e Mouse click event.
*/
protected iconClick_(e: MouseEvent) {
protected iconClick_(e: PointerEvent) {
if (this.getBlock().workspace.isDragging()) {
// Drag operation is concluding. Don't open the editor.
return;
@@ -179,14 +179,14 @@ export abstract class Icon {
// No-op on base class.
/**
* Show or hide the icon.
* Show or hide the bubble.
*
* @param _visible True if the icon should be visible.
* @param _visible True if the bubble should be visible.
*/
setVisible(_visible: boolean) {}
/**
* Returns the block this icon is attached to.
* @returns The block this icon is attached to.
*/
protected getBlock(): BlockSvg {
if (!this.block_) {

View File

@@ -393,7 +393,7 @@ function loadSounds(pathToMedia: string, workspace: WorkspaceSvg) {
// Android ignores any sound not loaded as a result of a user action.
soundBinds.push(browserEvents.conditionalBind(
document, 'mousemove', null, unbindSounds, true));
document, 'pointermove', null, unbindSounds, true));
soundBinds.push(browserEvents.conditionalBind(
document, 'touchstart', null, unbindSounds, true));
}

View File

@@ -30,12 +30,13 @@ import type {RenderedConnection} from './rendered_connection.js';
* @alias Blockly.Input
*/
export class Input {
private sourceBlock_: Block;
private sourceBlock: Block;
fieldRow: Field[] = [];
align: Align;
/** Alignment of input's fields (left, right or centre). */
align = Align.LEFT;
/** Is the input visible? */
private visible_ = true;
private visible = true;
/**
* @param type The type of the input.
@@ -51,19 +52,16 @@ export class Input {
throw Error(
'Value inputs and statement inputs must have non-empty name.');
}
this.sourceBlock_ = block;
/** Alignment of input's fields (left, right or centre). */
this.align = Align.LEFT;
this.sourceBlock = block;
}
/**
* Get the source block for this input.
*
* @returns The source block, or null if there is none.
* @returns The block this input is part of.
*/
getSourceBlock(): Block {
return this.sourceBlock_;
return this.sourceBlock;
}
/**
@@ -75,7 +73,7 @@ export class Input {
* field again. Should be unique to the host block.
* @returns The input being append to (to allow chaining).
*/
appendField(field: string|Field, opt_name?: string): Input {
appendField<T>(field: string|Field<T>, opt_name?: string): Input {
this.insertFieldAt(this.fieldRow.length, field, opt_name);
return this;
}
@@ -90,7 +88,8 @@ export class Input {
* field again. Should be unique to the host block.
* @returns The index following the last inserted field.
*/
insertFieldAt(index: number, field: string|Field, opt_name?: string): number {
insertFieldAt<T>(index: number, field: string|Field<T>, opt_name?: string):
number {
if (index < 0 || index > this.fieldRow.length) {
throw Error('index ' + index + ' out of bounds.');
}
@@ -103,13 +102,13 @@ export class Input {
// Generate a FieldLabel when given a plain text field.
if (typeof field === 'string') {
field = fieldRegistry.fromJson({
'type': 'field_label',
'text': field,
}) as Field;
type: 'field_label',
text: field,
})!;
}
field.setSourceBlock(this.sourceBlock_);
if (this.sourceBlock_.rendered) {
field.setSourceBlock(this.sourceBlock);
if (this.sourceBlock.rendered) {
field.init();
field.applyColour();
}
@@ -121,17 +120,17 @@ export class Input {
index = this.insertFieldAt(index, field.prefixField);
}
// Add the field to the field row.
this.fieldRow.splice(index, 0, field);
this.fieldRow.splice(index, 0, field as Field);
index++;
if (field.suffixField) {
// Add any suffix.
index = this.insertFieldAt(index, field.suffixField);
}
if (this.sourceBlock_.rendered) {
(this.sourceBlock_ as BlockSvg).render();
if (this.sourceBlock.rendered) {
(this.sourceBlock as BlockSvg).render();
// Adding a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours();
this.sourceBlock.bumpNeighbours();
}
return index;
}
@@ -150,10 +149,10 @@ export class Input {
if (field.name === name) {
field.dispose();
this.fieldRow.splice(i, 1);
if (this.sourceBlock_.rendered) {
(this.sourceBlock_ as BlockSvg).render();
if (this.sourceBlock.rendered) {
(this.sourceBlock as BlockSvg).render();
// Removing a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours();
this.sourceBlock.bumpNeighbours();
}
return true;
}
@@ -170,7 +169,7 @@ export class Input {
* @returns True if visible.
*/
isVisible(): boolean {
return this.visible_;
return this.visible;
}
/**
@@ -186,10 +185,10 @@ export class Input {
// because this function is package. If this function goes back to being a
// public API tests (lots of tests) should be added.
let renderList: AnyDuringMigration[] = [];
if (this.visible_ === visible) {
if (this.visible === visible) {
return renderList;
}
this.visible_ = visible;
this.visible = visible;
for (let y = 0, field; field = this.fieldRow[y]; y++) {
field.setVisible(visible);
@@ -245,8 +244,8 @@ export class Input {
*/
setAlign(align: Align): Input {
this.align = align;
if (this.sourceBlock_.rendered) {
const sourceBlock = this.sourceBlock_ as BlockSvg;
if (this.sourceBlock.rendered) {
const sourceBlock = this.sourceBlock as BlockSvg;
sourceBlock.render();
}
return this;
@@ -280,7 +279,7 @@ export class Input {
/** Initialize the fields on this input. */
init() {
if (!this.sourceBlock_.workspace.rendered) {
if (!this.sourceBlock.workspace.rendered) {
return; // Headless blocks don't need fields initialized.
}
for (let i = 0; i < this.fieldRow.length; i++) {

View File

@@ -29,8 +29,17 @@ import type {WorkspaceSvg} from './workspace_svg.js';
/** Represents a nearby valid connection. */
interface CandidateConnection {
closest: RenderedConnection|null;
local: RenderedConnection|null;
/**
* A nearby valid connection that is compatible with local.
* This is not on any of the blocks that are being dragged.
*/
closest: RenderedConnection;
/**
* A connection on the dragging stack that is compatible with closest. This is
* on the top block that is being dragged or the last block in the dragging
* stack.
*/
local: RenderedConnection;
radius: number;
}
@@ -51,87 +60,81 @@ const DUPLICATE_BLOCK_ERROR = 'The insertion marker ' +
* @alias Blockly.InsertionMarkerManager
*/
export class InsertionMarkerManager {
private readonly topBlock_: BlockSvg;
private readonly workspace_: WorkspaceSvg;
/**
* The top block in the stack being dragged.
* Does not change during a drag.
*/
private readonly topBlock: BlockSvg;
/**
* The workspace on which these connections are being dragged.
* Does not change during a drag.
*/
private readonly workspace: WorkspaceSvg;
/**
* The last connection on the stack, if it's not the last connection on the
* first block.
* Set in initAvailableConnections, if at all.
*/
private lastOnStack_: RenderedConnection|null = null;
private lastOnStack: RenderedConnection|null = null;
/**
* The insertion marker corresponding to the last block in the stack, if
* that's not the same as the first block in the stack.
* Set in initAvailableConnections, if at all
*/
private lastMarker_: BlockSvg|null = null;
private firstMarker_: BlockSvg;
private lastMarker: BlockSvg|null = null;
/**
* The connection that this block would connect to if released immediately.
* Updated on every mouse move.
* This is not on any of the blocks that are being dragged.
* The insertion marker that shows up between blocks to show where a block
* would go if dropped immediately.
*/
private closestConnection_: RenderedConnection|null = null;
private firstMarker: BlockSvg;
/**
* The connection that would connect to this.closestConnection_ if this
* block were released immediately. Updated on every mouse move. This is on
* the top block that is being dragged or the last block in the dragging
* stack.
* Information about the connection that would be made if the dragging block
* were released immediately. Updated on every mouse move.
*/
private localConnection_: RenderedConnection|null = null;
private activeCandidate: CandidateConnection|null = null;
/**
* Whether the block would be deleted if it were dropped immediately.
* Updated on every mouse move.
*
* @internal
*/
private wouldDeleteBlock_ = false;
public wouldDeleteBlock = false;
/**
* Connection on the insertion marker block that corresponds to
* this.localConnection_ on the currently dragged block.
* the active candidate's local connection on the currently dragged block.
*/
private markerConnection_: RenderedConnection|null = null;
private markerConnection: RenderedConnection|null = null;
/** The block that currently has an input being highlighted, or null. */
private highlightedBlock_: BlockSvg|null = null;
private highlightedBlock: BlockSvg|null = null;
/** The block being faded to indicate replacement, or null. */
private fadedBlock_: BlockSvg|null = null;
private availableConnections_: RenderedConnection[];
private fadedBlock: BlockSvg|null = null;
/**
* The connections on the dragging blocks that are available to connect to
* other blocks. This includes all open connections on the top block, as
* well as the last connection on the block stack.
*/
private availableConnections: RenderedConnection[];
/** @param block The top block in the stack being dragged. */
constructor(block: BlockSvg) {
common.setSelected(block);
this.topBlock = block;
/**
* The top block in the stack being dragged.
* Does not change during a drag.
*/
this.topBlock_ = block;
this.workspace = block.workspace;
/**
* The workspace on which these connections are being dragged.
* Does not change during a drag.
*/
this.workspace_ = block.workspace;
this.firstMarker = this.createMarkerBlock(this.topBlock);
/**
* The insertion marker that shows up between blocks to show where a block
* would go if dropped immediately.
*/
this.firstMarker_ = this.createMarkerBlock_(this.topBlock_);
/**
* The connections on the dragging blocks that are available to connect to
* other blocks. This includes all open connections on the top block, as
* well as the last connection on the block stack. Does not change during a
* drag.
*/
this.availableConnections_ = this.initAvailableConnections_();
this.availableConnections = this.initAvailableConnections();
}
/**
@@ -140,15 +143,15 @@ export class InsertionMarkerManager {
* @internal
*/
dispose() {
this.availableConnections_.length = 0;
this.availableConnections.length = 0;
eventUtils.disable();
try {
if (this.firstMarker_) {
this.firstMarker_.dispose();
if (this.firstMarker) {
this.firstMarker.dispose();
}
if (this.lastMarker_) {
this.lastMarker_.dispose();
if (this.lastMarker) {
this.lastMarker.dispose();
}
} finally {
eventUtils.enable();
@@ -162,18 +165,7 @@ export class InsertionMarkerManager {
* @internal
*/
updateAvailableConnections() {
this.availableConnections_ = this.initAvailableConnections_();
}
/**
* Return whether the block would be deleted if dropped immediately, based on
* information from the most recent move event.
*
* @returns True if the block would be deleted if dropped immediately.
* @internal
*/
wouldDeleteBlock(): boolean {
return this.wouldDeleteBlock_;
this.availableConnections = this.initAvailableConnections();
}
/**
@@ -184,7 +176,7 @@ export class InsertionMarkerManager {
* @internal
*/
wouldConnectBlock(): boolean {
return !!this.closestConnection_;
return !!this.activeCandidate;
}
/**
@@ -194,26 +186,21 @@ export class InsertionMarkerManager {
* @internal
*/
applyConnections() {
if (!this.closestConnection_) return;
if (!this.localConnection_) {
throw new Error(
'Cannot apply connections because there is no local connection');
}
if (!this.activeCandidate) return;
// Don't fire events for insertion markers.
eventUtils.disable();
this.hidePreview_();
this.hidePreview();
eventUtils.enable();
const {local, closest} = this.activeCandidate;
// Connect two blocks together.
this.localConnection_.connect(this.closestConnection_);
if (this.topBlock_.rendered) {
local.connect(closest);
if (this.topBlock.rendered) {
// Trigger a connection animation.
// Determine which connection is inferior (lower in the source stack).
const inferiorConnection = this.localConnection_.isSuperior() ?
this.closestConnection_ :
this.localConnection_;
const inferiorConnection = local.isSuperior() ? closest : local;
blockAnimations.connectionUiEffect(inferiorConnection.getSourceBlock());
// Bring the just-edited stack to the front.
const rootBlock = this.topBlock_.getRootBlock();
const rootBlock = this.topBlock.getRootBlock();
rootBlock.bringToFront();
}
}
@@ -226,18 +213,18 @@ export class InsertionMarkerManager {
* @internal
*/
update(dxy: Coordinate, dragTarget: IDragTarget|null) {
const candidate = this.getCandidate_(dxy);
const newCandidate = this.getCandidate(dxy);
this.wouldDeleteBlock_ = this.shouldDelete_(candidate, dragTarget);
this.wouldDeleteBlock = this.shouldDelete(!!newCandidate, dragTarget);
const shouldUpdate =
this.wouldDeleteBlock_ || this.shouldUpdatePreviews_(candidate, dxy);
this.wouldDeleteBlock || this.shouldUpdatePreviews(newCandidate, dxy);
if (shouldUpdate) {
// Don't fire events for insertion marker creation or movement.
eventUtils.disable();
this.maybeHidePreview_(candidate);
this.maybeShowPreview_(candidate);
this.maybeHidePreview(newCandidate);
this.maybeShowPreview(newCandidate);
eventUtils.enable();
}
}
@@ -248,13 +235,13 @@ export class InsertionMarkerManager {
* @param sourceBlock The block that the insertion marker will represent.
* @returns The insertion marker that represents the given block.
*/
private createMarkerBlock_(sourceBlock: BlockSvg): BlockSvg {
private createMarkerBlock(sourceBlock: BlockSvg): BlockSvg {
const imType = sourceBlock.type;
eventUtils.disable();
let result: BlockSvg;
try {
result = this.workspace_.newBlock(imType);
result = this.workspace.newBlock(imType);
result.setInsertionMarker(true);
if (sourceBlock.saveExtraState) {
const state = sourceBlock.saveExtraState();
@@ -304,27 +291,27 @@ export class InsertionMarkerManager {
/**
* Populate the list of available connections on this block stack. This
* should only be called once, at the beginning of a drag. If the stack has
* more than one block, this function will populate lastOnStack_ and create
* more than one block, this function will populate lastOnStack and create
* the corresponding insertion marker.
*
* @returns A list of available connections.
*/
private initAvailableConnections_(): RenderedConnection[] {
const available = this.topBlock_.getConnections_(false);
private initAvailableConnections(): RenderedConnection[] {
const available = this.topBlock.getConnections_(false);
// Also check the last connection on this stack
const lastOnStack = this.topBlock_.lastConnectionInStack(true);
if (lastOnStack && lastOnStack !== this.topBlock_.nextConnection) {
const lastOnStack = this.topBlock.lastConnectionInStack(true);
if (lastOnStack && lastOnStack !== this.topBlock.nextConnection) {
available.push(lastOnStack);
this.lastOnStack_ = lastOnStack;
if (this.lastMarker_) {
this.lastOnStack = lastOnStack;
if (this.lastMarker) {
eventUtils.disable();
try {
this.lastMarker_.dispose();
this.lastMarker.dispose();
} finally {
eventUtils.enable();
}
}
this.lastMarker_ = this.createMarkerBlock_(lastOnStack.getSourceBlock());
this.lastMarker = this.createMarkerBlock(lastOnStack.getSourceBlock());
}
return available;
}
@@ -333,51 +320,34 @@ export class InsertionMarkerManager {
* Whether the previews (insertion marker and replacement marker) should be
* updated based on the closest candidate and the current drag distance.
*
* @param candidate An object containing a local connection, a closest
* connection, and a radius. Returned by getCandidate_.
* @param newCandidate A new candidate connection that may replace the current
* best candidate.
* @param dxy Position relative to drag start, in workspace units.
* @returns Whether the preview should be updated.
*/
private shouldUpdatePreviews_(
candidate: CandidateConnection, dxy: Coordinate): boolean {
const candidateLocal = candidate.local;
const candidateClosest = candidate.closest;
const radius = candidate.radius;
private shouldUpdatePreviews(
newCandidate: CandidateConnection|null, dxy: Coordinate): boolean {
// Only need to update if we were showing a preview before.
if (!newCandidate) return !!this.activeCandidate;
// Found a connection!
if (candidateLocal && candidateClosest) {
// We're already showing an insertion marker.
// Decide whether the new connection has higher priority.
if (this.localConnection_ && this.closestConnection_) {
// The connection was the same as the current connection.
if (this.closestConnection_ === candidateClosest &&
this.localConnection_ === candidateLocal) {
return false;
}
const xDiff =
this.localConnection_.x + dxy.x - this.closestConnection_.x;
const yDiff =
this.localConnection_.y + dxy.y - this.closestConnection_.y;
const curDistance = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
// Slightly prefer the existing preview over a new preview.
return !(
candidateClosest &&
radius > curDistance - config.currentConnectionPreference);
} else if (!this.localConnection_ && !this.closestConnection_) {
// We weren't showing a preview before, but we should now.
return true;
} else {
console.error(
'Only one of localConnection_ and closestConnection_ was set.');
}
} else { // No connection found.
// Only need to update if we were showing a preview before.
return !!(this.localConnection_ && this.closestConnection_);
// We weren't showing a preview before, but we should now.
if (!this.activeCandidate) return true;
// We're already showing an insertion marker.
// Decide whether the new connection has higher priority.
const {local: activeLocal, closest: activeClosest} = this.activeCandidate;
if (activeClosest === newCandidate.closest &&
activeLocal === newCandidate.local) {
// The connection was the same as the current connection.
return false;
}
console.error(
'Returning true from shouldUpdatePreviews, but it\'s not clear why.');
return true;
const xDiff = activeLocal.x + dxy.x - activeClosest.x;
const yDiff = activeLocal.y + dxy.y - activeClosest.y;
const curDistance = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
// Slightly prefer the existing preview over a new preview.
return (
newCandidate.radius < curDistance - config.currentConnectionPreference);
}
/**
@@ -388,11 +358,7 @@ export class InsertionMarkerManager {
* @returns An object containing a local connection, a closest connection, and
* a radius.
*/
private getCandidate_(dxy: Coordinate): CandidateConnection {
let radius = this.getStartRadius_();
let candidateClosest = null;
let candidateLocal = null;
private getCandidate(dxy: Coordinate): CandidateConnection|null {
// It's possible that a block has added or removed connections during a
// drag, (e.g. in a drag/move event handler), so let's update the available
// connections. Note that this will be called on every move while dragging,
@@ -400,20 +366,25 @@ export class InsertionMarkerManager {
// so, maybe it could be made more efficient. Also note that we won't update
// the connections if we've already connected the insertion marker to a
// block.
if (!this.markerConnection_ || !this.markerConnection_.isConnected()) {
if (!this.markerConnection || !this.markerConnection.isConnected()) {
this.updateAvailableConnections();
}
for (let i = 0; i < this.availableConnections_.length; i++) {
const myConnection = this.availableConnections_[i];
let radius = this.getStartRadius();
let candidate = null;
for (let i = 0; i < this.availableConnections.length; i++) {
const myConnection = this.availableConnections[i];
const neighbour = myConnection.closest(radius, dxy);
if (neighbour.connection) {
candidateClosest = neighbour.connection;
candidateLocal = myConnection;
candidate = {
closest: neighbour.connection,
local: myConnection,
radius: neighbour.radius,
};
radius = neighbour.radius;
}
}
return {closest: candidateClosest, local: candidateLocal, radius};
return candidate;
}
/**
@@ -422,36 +393,34 @@ export class InsertionMarkerManager {
* @returns The radius at which to start the search for the closest
* connection.
*/
private getStartRadius_(): number {
private getStartRadius(): number {
// If there is already a connection highlighted,
// increase the radius we check for making new connections.
// Why? When a connection is highlighted, blocks move around when the
// When a connection is highlighted, blocks move around when the
// insertion marker is created, which could cause the connection became out
// of range. By increasing radiusConnection when a connection already
// exists, we never "lose" the connection from the offset.
if (this.closestConnection_ && this.localConnection_) {
return config.connectingSnapRadius;
}
return config.snapRadius;
return this.activeCandidate ? config.connectingSnapRadius :
config.snapRadius;
}
/**
* Whether ending the drag would delete the block.
*
* @param candidate An object containing a local connection, a closest
* connection, and a radius.
* @param newCandidate Whether there is a candidate connection that the
* block could connect to if the drag ended immediately.
* @param dragTarget The drag target that the block is currently over.
* @returns Whether dropping the block immediately would delete the block.
*/
private shouldDelete_(
candidate: CandidateConnection, dragTarget: IDragTarget|null): boolean {
private shouldDelete(newCandidate: boolean, dragTarget: IDragTarget|null):
boolean {
if (dragTarget) {
const componentManager = this.workspace_.getComponentManager();
const componentManager = this.workspace.getComponentManager();
const isDeleteArea = componentManager.hasCapability(
dragTarget.id, ComponentManager.Capability.DELETE_AREA);
if (isDeleteArea) {
return (dragTarget as IDeleteArea)
.wouldDelete(this.topBlock_, candidate && !!candidate.closest);
.wouldDelete(this.topBlock, newCandidate);
}
}
return false;
@@ -460,150 +429,122 @@ export class InsertionMarkerManager {
/**
* Show an insertion marker or replacement highlighting during a drag, if
* needed.
* At the beginning of this function, this.localConnection_ and
* this.closestConnection_ should both be null.
* At the beginning of this function, this.activeConnection should be null.
*
* @param candidate An object containing a local connection, a closest
* connection, and a radius.
* @param newCandidate A new candidate connection that may replace the current
* best candidate.
*/
private maybeShowPreview_(candidate: CandidateConnection) {
// Nope, don't add a marker.
if (this.wouldDeleteBlock_) {
return;
}
const closest = candidate.closest;
const local = candidate.local;
private maybeShowPreview(newCandidate: CandidateConnection|null) {
if (this.wouldDeleteBlock) return; // Nope, don't add a marker.
if (!newCandidate) return; // Nothing to connect to.
// Nothing to connect to.
if (!closest) {
return;
}
const closest = newCandidate.closest;
// Something went wrong and we're trying to connect to an invalid
// connection.
if (closest === this.closestConnection_ ||
if (closest === this.activeCandidate?.closest ||
closest.getSourceBlock().isInsertionMarker()) {
console.log('Trying to connect to an insertion marker');
return;
}
this.activeCandidate = newCandidate;
// Add an insertion marker or replacement marker.
this.closestConnection_ = closest;
this.localConnection_ = local;
this.showPreview_();
this.showPreview(this.activeCandidate);
}
/**
* A preview should be shown. This function figures out if it should be a
* block highlight or an insertion marker, and shows the appropriate one.
*
* @param activeCandidate The connection that will be made if the drag ends
* immediately.
*/
private showPreview_() {
if (!this.closestConnection_) {
throw new Error(
'Cannot show the preview because there is no closest connection');
}
if (!this.localConnection_) {
throw new Error(
'Cannot show the preview because there is no local connection');
}
const closest = this.closestConnection_;
const renderer = this.workspace_.getRenderer();
private showPreview(activeCandidate: CandidateConnection) {
const renderer = this.workspace.getRenderer();
const method = renderer.getConnectionPreviewMethod(
closest, this.localConnection_, this.topBlock_);
activeCandidate.closest, activeCandidate.local, this.topBlock);
switch (method) {
case InsertionMarkerManager.PREVIEW_TYPE.INPUT_OUTLINE:
this.showInsertionInputOutline_();
this.showInsertionInputOutline(activeCandidate);
break;
case InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER:
this.showInsertionMarker_();
this.showInsertionMarker(activeCandidate);
break;
case InsertionMarkerManager.PREVIEW_TYPE.REPLACEMENT_FADE:
this.showReplacementFade_();
this.showReplacementFade(activeCandidate);
break;
}
// Optionally highlight the actual connection, as a nod to previous
// behaviour.
if (closest && renderer.shouldHighlightConnection(closest)) {
closest.highlight();
if (renderer.shouldHighlightConnection(activeCandidate.closest)) {
activeCandidate.closest.highlight();
}
}
/**
* Show an insertion marker or replacement highlighting during a drag, if
* Hide an insertion marker or replacement highlighting during a drag, if
* needed.
* At the end of this function, this.localConnection_ and
* this.closestConnection_ should both be null.
* At the end of this function, this.activeCandidate will be null.
*
* @param candidate An object containing a local connection, a closest
* connection, and a radius.
* @param newCandidate A new candidate connection that may replace the current
* best candidate.
*/
private maybeHidePreview_(candidate: CandidateConnection) {
private maybeHidePreview(newCandidate: CandidateConnection|null) {
// If there's no new preview, remove the old one but don't bother deleting
// it. We might need it later, and this saves disposing of it and recreating
// it.
if (!candidate.closest) {
this.hidePreview_();
if (!newCandidate) {
this.hidePreview();
} else {
// If there's a new preview and there was an preview before, and either
// connection has changed, remove the old preview.
const hadPreview = this.closestConnection_ && this.localConnection_;
const closestChanged = this.closestConnection_ !== candidate.closest;
const localChanged = this.localConnection_ !== candidate.local;
if (this.activeCandidate) {
const closestChanged =
this.activeCandidate.closest !== newCandidate.closest;
const localChanged = this.activeCandidate.local !== newCandidate.local;
// Also hide if we had a preview before but now we're going to delete
// instead.
if (hadPreview &&
(closestChanged || localChanged || this.wouldDeleteBlock_)) {
this.hidePreview_();
// If there's a new preview and there was a preview before, and either
// connection has changed, remove the old preview.
// Also hide if we had a preview before but now we're going to delete
// instead.
if ((closestChanged || localChanged || this.wouldDeleteBlock)) {
this.hidePreview();
}
}
}
// Either way, clear out old state.
this.markerConnection_ = null;
this.closestConnection_ = null;
this.localConnection_ = null;
this.markerConnection = null;
this.activeCandidate = null;
}
/**
* A preview should be hidden. This function figures out if it is a block
* highlight or an insertion marker, and hides the appropriate one.
* A preview should be hidden. Loop through all possible preview modes
* and hide everything.
*/
private hidePreview_() {
if (this.closestConnection_ && this.closestConnection_.targetBlock() &&
this.workspace_.getRenderer().shouldHighlightConnection(
this.closestConnection_)) {
this.closestConnection_.unhighlight();
}
if (this.fadedBlock_) {
this.hideReplacementFade_();
} else if (this.highlightedBlock_) {
this.hideInsertionInputOutline_();
} else if (this.markerConnection_) {
this.hideInsertionMarker_();
private hidePreview() {
const closest = this.activeCandidate?.closest;
if (closest && closest.targetBlock() &&
this.workspace.getRenderer().shouldHighlightConnection(closest)) {
closest.unhighlight();
}
this.hideReplacementFade();
this.hideInsertionInputOutline();
this.hideInsertionMarker();
}
/**
* Shows an insertion marker connected to the appropriate blocks (based on
* manager state).
*
* @param activeCandidate The connection that will be made if the drag ends
* immediately.
*/
private showInsertionMarker_() {
if (!this.localConnection_) {
throw new Error(
'Cannot show the insertion marker because there is no local ' +
'connection');
}
if (!this.closestConnection_) {
throw new Error(
'Cannot show the insertion marker because there is no closest ' +
'connection');
}
const local = this.localConnection_;
const closest = this.closestConnection_;
private showInsertionMarker(activeCandidate: CandidateConnection) {
const {local, closest} = activeCandidate;
const isLastInStack = this.lastOnStack_ && local === this.lastOnStack_;
let insertionMarker = isLastInStack ? this.lastMarker_ : this.firstMarker_;
const isLastInStack = this.lastOnStack && local === this.lastOnStack;
let insertionMarker = isLastInStack ? this.lastMarker : this.firstMarker;
if (!insertionMarker) {
throw new Error(
'Cannot show the insertion marker because there is no insertion ' +
@@ -620,8 +561,8 @@ export class InsertionMarkerManager {
// probably recreate the marker block (e.g. in getCandidate_), which is
// called more often during the drag, but creating a block that often
// might be too slow, so we only do it if necessary.
this.firstMarker_ = this.createMarkerBlock_(this.topBlock_);
insertionMarker = isLastInStack ? this.lastMarker_ : this.firstMarker_;
this.firstMarker = this.createMarkerBlock(this.topBlock);
insertionMarker = isLastInStack ? this.lastMarker : this.firstMarker;
if (!insertionMarker) {
throw new Error(
'Cannot show the insertion marker because there is no insertion ' +
@@ -637,7 +578,7 @@ export class InsertionMarkerManager {
'associated connection');
}
if (imConn === this.markerConnection_) {
if (imConn === this.markerConnection) {
throw new Error(
'Made it to showInsertionMarker_ even though the marker isn\'t ' +
'changing');
@@ -658,39 +599,37 @@ export class InsertionMarkerManager {
imConn.connect(closest);
}
this.markerConnection_ = imConn;
this.markerConnection = imConn;
}
/**
* Disconnects and hides the current insertion marker. Should return the
* blocks to their original state.
*/
private hideInsertionMarker_() {
if (!this.markerConnection_) {
console.log('No insertion marker connection to disconnect');
return;
}
private hideInsertionMarker() {
if (!this.markerConnection) return;
const imConn = this.markerConnection_;
const imBlock = imConn.getSourceBlock();
const markerConn = this.markerConnection;
const imBlock = markerConn.getSourceBlock();
const markerNext = imBlock.nextConnection;
const markerPrev = imBlock.previousConnection;
const markerOutput = imBlock.outputConnection;
const isFirstInStatementStack =
imConn === markerNext && !(markerPrev && markerPrev.targetConnection);
const isNext = markerConn === markerNext;
const isFirstInOutputStack = imConn.type === ConnectionType.INPUT_VALUE &&
const isFirstInStatementStack =
isNext && !(markerPrev && markerPrev.targetConnection);
const isFirstInOutputStack =
markerConn.type === ConnectionType.INPUT_VALUE &&
!(markerOutput && markerOutput.targetConnection);
// The insertion marker is the first block in a stack. Unplug won't do
// anything in that case. Instead, unplug the following block.
if (isFirstInStatementStack || isFirstInOutputStack) {
imConn.targetBlock()!.unplug(false);
} else if (
imConn.type === ConnectionType.NEXT_STATEMENT &&
imConn !== markerNext) {
markerConn.targetBlock()!.unplug(false);
} else if (markerConn.type === ConnectionType.NEXT_STATEMENT && !isNext) {
// Inside of a C-block, first statement connection.
const innerConnection = imConn.targetConnection;
const innerConnection = markerConn.targetConnection;
if (innerConnection) {
innerConnection.getSourceBlock().unplug(false);
}
@@ -703,80 +642,73 @@ export class InsertionMarkerManager {
previousBlockNextConnection.connect(innerConnection);
}
} else {
imBlock.unplug(/* healStack */
true);
imBlock.unplug(/* healStack */ true);
}
if (imConn.targetConnection) {
if (markerConn.targetConnection) {
throw Error(
'markerConnection_ still connected at the end of ' +
'markerConnection still connected at the end of ' +
'disconnectInsertionMarker');
}
this.markerConnection_ = null;
this.markerConnection = null;
const svg = imBlock.getSvgRoot();
if (svg) {
svg.setAttribute('visibility', 'hidden');
}
}
/** Shows an outline around the input the closest connection belongs to. */
private showInsertionInputOutline_() {
if (!this.closestConnection_) {
throw new Error(
'Cannot show the insertion marker outline because ' +
'there is no closest connection');
}
const closest = this.closestConnection_;
this.highlightedBlock_ = closest.getSourceBlock();
this.highlightedBlock_.highlightShapeForInput(closest, true);
/**
* Shows an outline around the input the closest connection belongs to.
*
* @param activeCandidate The connection that will be made if the drag ends
* immediately.
*/
private showInsertionInputOutline(activeCandidate: CandidateConnection) {
const closest = activeCandidate.closest;
this.highlightedBlock = closest.getSourceBlock();
this.highlightedBlock.highlightShapeForInput(closest, true);
}
/** Hides any visible input outlines. */
private hideInsertionInputOutline_() {
if (!this.highlightedBlock_) {
private hideInsertionInputOutline() {
if (!this.highlightedBlock) return;
if (!this.activeCandidate) {
throw new Error(
'Cannot hide the insertion marker outline because ' +
'there is no highlighted block');
'there is no active candidate');
}
if (!this.closestConnection_) {
throw new Error(
'Cannot hide the insertion marker outline because ' +
'there is no closest connection');
}
this.highlightedBlock_.highlightShapeForInput(
this.closestConnection_, false);
this.highlightedBlock_ = null;
this.highlightedBlock.highlightShapeForInput(
this.activeCandidate.closest, false);
this.highlightedBlock = null;
}
/**
* Shows a replacement fade affect on the closest connection's target block
* (the block that is currently connected to it).
*
* @param activeCandidate The connection that will be made if the drag ends
* immediately.
*/
private showReplacementFade_() {
if (!this.closestConnection_) {
throw new Error(
'Cannot show the replacement fade because there ' +
'is no closest connection');
}
this.fadedBlock_ = this.closestConnection_.targetBlock();
if (!this.fadedBlock_) {
private showReplacementFade(activeCandidate: CandidateConnection) {
this.fadedBlock = activeCandidate.closest.targetBlock();
if (!this.fadedBlock) {
throw new Error(
'Cannot show the replacement fade because the ' +
'closest connection does not have a target block');
}
this.fadedBlock_.fadeForReplacement(true);
this.fadedBlock.fadeForReplacement(true);
}
/** Hides/Removes any visible fade affects. */
private hideReplacementFade_() {
if (!this.fadedBlock_) {
throw new Error(
'Cannot hide the replacement because there is no ' +
'faded block');
}
this.fadedBlock_.fadeForReplacement(false);
this.fadedBlock_ = null;
/**
* Hides/Removes any visible fade affects.
*/
private hideReplacementFade() {
if (!this.fadedBlock) return;
this.fadedBlock.fadeForReplacement(false);
this.fadedBlock = null;
}
/**
@@ -788,11 +720,11 @@ export class InsertionMarkerManager {
*/
getInsertionMarkers(): BlockSvg[] {
const result = [];
if (this.firstMarker_) {
result.push(this.firstMarker_);
if (this.firstMarker) {
result.push(this.firstMarker);
}
if (this.lastMarker_) {
result.push(this.lastMarker_);
if (this.lastMarker) {
result.push(this.lastMarker);
}
return result;
}

View File

@@ -0,0 +1,25 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* An object that fires events optionally.
*
* @internal
*/
export interface IObservable {
startPublishing(): void;
stopPublishing(): void;
}
/**
* Type guard for checking if an object fulfills IObservable.
*
* @internal
*/
export function isObservable(obj: any): obj is IObservable {
return obj.startPublishing !== undefined && obj.stopPublishing !== undefined;
}

View File

@@ -4,11 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* The interface for the data model of a procedure parameter.
*
* @namespace Blockly.IParameterModel
*/
import {IProcedureModel} from './i_procedure_model';
/**
@@ -42,4 +38,7 @@ export interface IParameterModel {
* over time.
*/
getId(): string;
/** Sets the procedure model this parameter is associated with. */
setProcedureModel(model: IProcedureModel): this;
}

View File

@@ -1,29 +0,0 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* The interface for a Blockly field that can be registered.
*
* @namespace Blockly.IRegistrableField
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.IRegistrableField');
import type {Field} from '../field.js';
type fromJson = (p1: object) => Field;
/**
* A registrable field.
* Note: We are not using an interface here as we are interested in defining the
* static methods of a field rather than the instance methods.
*
* @alias Blockly.IRegistrableField
*/
export interface IRegistrableField {
fromJson: fromJson;
}

View File

@@ -105,13 +105,13 @@ export class Menu {
// Add event handlers.
this.mouseOverHandler = browserEvents.conditionalBind(
element, 'mouseover', this, this.handleMouseOver, true);
element, 'pointerover', this, this.handleMouseOver, true);
this.clickHandler = browserEvents.conditionalBind(
element, 'click', this, this.handleClick, true);
element, 'pointerdown', this, this.handleClick, true);
this.mouseEnterHandler = browserEvents.conditionalBind(
element, 'mouseenter', this, this.handleMouseEnter, true);
element, 'pointerenter', this, this.handleMouseEnter, true);
this.mouseLeaveHandler = browserEvents.conditionalBind(
element, 'mouseleave', this, this.handleMouseLeave, true);
element, 'pointerleave', this, this.handleMouseLeave, true);
this.onKeyDownHandler = browserEvents.conditionalBind(
element, 'keydown', this, this.handleKeyEvent);
@@ -310,7 +310,7 @@ export class Menu {
*
* @param e Mouse event to handle.
*/
private handleMouseOver(e: Event) {
private handleMouseOver(e: PointerEvent) {
const menuItem = this.getMenuItem(e.target as Element);
if (menuItem) {
@@ -329,18 +329,12 @@ export class Menu {
*
* @param e Click event to handle.
*/
private handleClick(e: Event) {
private handleClick(e: PointerEvent) {
const oldCoords = this.openingCoords;
// Clear out the saved opening coords immediately so they're not used twice.
this.openingCoords = null;
// AnyDuringMigration because: Property 'clientX' does not exist on type
// 'Event'.
if (oldCoords && typeof (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'.
const newCoords = new Coordinate(
(e as AnyDuringMigration).clientX, (e as AnyDuringMigration).clientY);
if (oldCoords && typeof e.clientX === 'number') {
const newCoords = new Coordinate(e.clientX, e.clientY);
if (Coordinate.distance(oldCoords, newCoords) < 1) {
// This menu was opened by a mousedown and we're handling the consequent
// click event. The coords haven't changed, meaning this was the same
@@ -362,7 +356,7 @@ export class Menu {
*
* @param _e Mouse event to handle.
*/
private handleMouseEnter(_e: Event) {
private handleMouseEnter(_e: PointerEvent) {
this.focus();
}
@@ -371,7 +365,7 @@ export class Menu {
*
* @param _e Mouse event to handle.
*/
private handleMouseLeave(_e: Event) {
private handleMouseLeave(_e: PointerEvent) {
if (this.getElement()) {
this.blur();
this.setHighlighted(null);

View File

@@ -155,7 +155,7 @@ export class Mutator extends Icon {
*
* @param e Mouse click event.
*/
protected override iconClick_(e: MouseEvent) {
protected override iconClick_(e: PointerEvent) {
if (this.getBlock().isEditable()) {
super.iconClick_(e);
}

View File

@@ -38,6 +38,7 @@ export class Options {
readOnly: boolean;
maxBlocks: number;
maxInstances: {[key: string]: number}|null;
modalInputs: boolean;
pathToMedia: string;
hasCategories: boolean;
moveOptions: MoveOptions;
@@ -155,6 +156,11 @@ export class Options {
const plugins = options['plugins'] || {};
let modalInputs = options['modalInputs'];
if (modalInputs === undefined) {
modalInputs = true;
}
this.RTL = rtl;
this.oneBasedIndex = oneBasedIndex;
this.collapse = hasCollapse;
@@ -163,6 +169,7 @@ export class Options {
this.readOnly = readOnly;
this.maxBlocks = options['maxBlocks'] || Infinity;
this.maxInstances = options['maxInstances'] ?? null;
this.modalInputs = modalInputs;
this.pathToMedia = pathToMedia;
this.hasCategories = hasCategories;
this.moveOptions = Options.parseMoveOptions_(options, hasCategories);

View File

@@ -4,8 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as eventUtils from '../events/utils.js';
import {genUid} from '../utils/idgenerator.js';
import type {IParameterModel} from '../interfaces/i_parameter_model.js';
import type {IProcedureModel} from '../interfaces/i_procedure_model';
import {triggerProceduresUpdate} from './update_procedures.js';
import type {VariableModel} from '../variable_model.js';
import type {Workspace} from '../workspace.js';
@@ -14,6 +16,8 @@ import type {Workspace} from '../workspace.js';
export class ObservableParameterModel implements IParameterModel {
private id: string;
private variable: VariableModel;
private shouldFireEvents = false;
private procedureModel: IProcedureModel|null = null;
constructor(
private readonly workspace: Workspace, name: string, id?: string) {
@@ -26,11 +30,16 @@ export class ObservableParameterModel implements IParameterModel {
* 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;
if (name === this.variable.name) return this;
const oldName = this.variable.name;
this.variable =
this.workspace.getVariable(name) ?? this.workspace.createVariable(name);
triggerProceduresUpdate(this.workspace);
if (this.shouldFireEvents) {
eventUtils.fire(
new (eventUtils.get(eventUtils.PROCEDURE_PARAMETER_RENAME))(
this.workspace, this.procedureModel, this, oldName));
}
return this;
}
@@ -76,4 +85,31 @@ export class ObservableParameterModel implements IParameterModel {
getVariableModel(): VariableModel {
return this.variable;
}
/**
* Tells the parameter model it should fire events.
*
* @internal
*/
startPublishing() {
this.shouldFireEvents = true;
}
/**
* Tells the parameter model it should not fire events.
*
* @internal
*/
stopPublishing() {
this.shouldFireEvents = false;
}
/** Sets the procedure model this parameter is a part of. */
setProcedureModel(model: IProcedureModel): this {
// TODO: Not sure if we want to do this, or accept it via the constructor.
// That means it could be non-null, but it would also break the fluent
// API.
this.procedureModel = model;
return this;
}
}

View File

@@ -4,10 +4,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as eventUtils from '../events/utils.js';
import {IProcedureMap} from '../interfaces/i_procedure_map.js';
import type {IProcedureModel} from '../interfaces/i_procedure_model.js';
import {isObservable} from '../interfaces/i_observable.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
@@ -20,8 +22,11 @@ export class ObservableProcedureMap extends
* Adds the given procedure model to the procedure map.
*/
override set(id: string, proc: IProcedureModel): this {
// TODO(#6516): Fire events.
if (this.get(id) === proc) return this;
super.set(id, proc);
eventUtils.fire(new (eventUtils.get(eventUtils.PROCEDURE_CREATE))(
this.workspace, proc));
if (isObservable(proc)) proc.startPublishing();
return this;
}
@@ -30,9 +35,13 @@ export class ObservableProcedureMap extends
* exists).
*/
override delete(id: string): boolean {
// TODO(#6516): Fire events.
const proc = this.get(id);
const existed = super.delete(id);
if (!existed) return existed;
triggerProceduresUpdate(this.workspace);
eventUtils.fire(new (eventUtils.get(eventUtils.PROCEDURE_DELETE))(
this.workspace, proc));
if (isObservable(proc)) proc.stopPublishing();
return existed;
}
@@ -40,8 +49,13 @@ export class ObservableProcedureMap extends
* Removes all ProcedureModels from the procedure map.
*/
override clear() {
// TODO(#6516): Fire events.
super.clear();
if (!this.size) return;
for (const id of this.keys()) {
const proc = this.get(id);
super.delete(id);
eventUtils.fire(new (eventUtils.get(eventUtils.PROCEDURE_DELETE))(
this.workspace, proc));
}
triggerProceduresUpdate(this.workspace);
}
@@ -50,7 +64,6 @@ export class ObservableProcedureMap extends
* 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);
}

View File

@@ -4,9 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as eventUtils from '../events/utils.js';
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 {isObservable} from '../interfaces/i_observable.js';
import {triggerProceduresUpdate} from './update_procedures.js';
import type {Workspace} from '../workspace.js';
@@ -17,6 +19,7 @@ export class ObservableProcedureModel implements IProcedureModel {
private parameters: IParameterModel[] = [];
private returnTypes: string[]|null = null;
private enabled = true;
private shouldFireEvents = false;
constructor(
private readonly workspace: Workspace, name: string, id?: string) {
@@ -26,9 +29,14 @@ export class ObservableProcedureModel implements IProcedureModel {
/** Sets the human-readable name of the procedure. */
setName(name: string): this {
// TODO(#6516): Fire events.
if (name === this.name) return this;
const prevName = this.name;
this.name = name;
triggerProceduresUpdate(this.workspace);
if (this.shouldFireEvents) {
eventUtils.fire(new (eventUtils.get(eventUtils.PROCEDURE_RENAME))(
this.workspace, this, prevName));
}
return this;
}
@@ -38,17 +46,46 @@ export class ObservableProcedureModel implements IProcedureModel {
* To move a parameter, first delete it, and then re-insert.
*/
insertParameter(parameterModel: IParameterModel, index: number): this {
// TODO(#6516): Fire events.
if (this.parameters[index] &&
this.parameters[index].getId() === parameterModel.getId()) {
return this;
}
this.parameters.splice(index, 0, parameterModel);
parameterModel.setProcedureModel(this);
if (isObservable(parameterModel)) {
if (this.shouldFireEvents) {
parameterModel.startPublishing();
} else {
parameterModel.stopPublishing();
}
}
triggerProceduresUpdate(this.workspace);
if (this.shouldFireEvents) {
eventUtils.fire(
new (eventUtils.get(eventUtils.PROCEDURE_PARAMETER_CREATE))(
this.workspace, this, parameterModel, index));
}
return this;
}
/** Removes the parameter at the given index from the parameter list. */
deleteParameter(index: number): this {
// TODO(#6516): Fire events.
if (!this.parameters[index]) return this;
const oldParam = this.parameters[index];
this.parameters.splice(index, 1);
triggerProceduresUpdate(this.workspace);
if (isObservable(oldParam)) {
oldParam.stopPublishing();
}
if (this.shouldFireEvents) {
eventUtils.fire(
new (eventUtils.get(eventUtils.PROCEDURE_PARAMETER_DELETE))(
this.workspace, this, oldParam, index));
}
return this;
}
@@ -67,9 +104,15 @@ export class ObservableProcedureModel implements IProcedureModel {
'The built-in ProcedureModel does not support typing. You need to ' +
'implement your own custom ProcedureModel.');
}
// Either they're both an empty array, or both null. Noop either way.
if (!!types === !!this.returnTypes) return this;
const oldReturnTypes = this.returnTypes;
this.returnTypes = types;
// TODO(#6516): Fire events.
triggerProceduresUpdate(this.workspace);
if (this.shouldFireEvents) {
eventUtils.fire(new (eventUtils.get(eventUtils.PROCEDURE_CHANGE_RETURN))(
this.workspace, this, oldReturnTypes));
}
return this;
}
@@ -78,9 +121,13 @@ export class ObservableProcedureModel implements IProcedureModel {
* all procedure caller blocks should be disabled as well.
*/
setEnabled(enabled: boolean): this {
// TODO(#6516): Fire events.
if (enabled === this.enabled) return this;
this.enabled = enabled;
triggerProceduresUpdate(this.workspace);
if (this.shouldFireEvents) {
eventUtils.fire(new (eventUtils.get(eventUtils.PROCEDURE_ENABLE))(
this.workspace, this));
}
return this;
}
@@ -120,4 +167,28 @@ export class ObservableProcedureModel implements IProcedureModel {
getEnabled(): boolean {
return this.enabled;
}
/**
* Tells the procedure model it should fire events.
*
* @internal
*/
startPublishing() {
this.shouldFireEvents = true;
for (const param of this.parameters) {
if (isObservable(param)) param.startPublishing();
}
}
/**
* Tells the procedure model it should not fire events.
*
* @internal
*/
stopPublishing() {
this.shouldFireEvents = false;
for (const param of this.parameters) {
if (isObservable(param)) param.stopPublishing();
}
}
}

View File

@@ -211,9 +211,9 @@ export class Scrollbar {
}
this.onMouseDownBarWrapper_ = browserEvents.conditionalBind(
this.svgBackground, 'mousedown', this, this.onMouseDownBar);
this.svgBackground, 'pointerdown', this, this.onMouseDownBar);
this.onMouseDownHandleWrapper_ = browserEvents.conditionalBind(
this.svgHandle, 'mousedown', this, this.onMouseDownHandle);
this.svgHandle, 'pointerdown', this, this.onMouseDownHandle);
}
/**
@@ -703,7 +703,7 @@ export class Scrollbar {
*
* @param e Mouse down event.
*/
private onMouseDownHandle(e: MouseEvent) {
private onMouseDownHandle(e: PointerEvent) {
this.workspace.markFocused();
this.cleanUp();
if (browserEvents.isRightButton(e)) {
@@ -723,9 +723,9 @@ export class Scrollbar {
// Record the current mouse position.
this.startDragMouse = this.horizontal ? e.clientX : e.clientY;
this.onMouseUpWrapper_ = browserEvents.conditionalBind(
document, 'mouseup', this, this.onMouseUpHandle);
document, 'pointerup', this, this.onMouseUpHandle);
this.onMouseMoveWrapper_ = browserEvents.conditionalBind(
document, 'mousemove', this, this.onMouseMoveHandle);
document, 'pointermove', this, this.onMouseMoveHandle);
e.stopPropagation();
e.preventDefault();
}
@@ -735,7 +735,7 @@ export class Scrollbar {
*
* @param e Mouse move event.
*/
private onMouseMoveHandle(e: MouseEvent) {
private onMouseMoveHandle(e: PointerEvent) {
const currentMouse = this.horizontal ? e.clientX : e.clientY;
const mouseDelta = currentMouse - this.startDragMouse;
const handlePosition = this.startDragHandle + mouseDelta;

View File

@@ -18,6 +18,7 @@ import type {Workspace} from '../workspace.js';
* Representation of a procedure data model.
*/
export interface State {
// TODO: This should also handle enabled.
id: string, name: string, returnTypes: string[]|null,
parameters?: ParameterState[],
}
@@ -50,8 +51,12 @@ type ParameterModelConstructor<ParameterModel extends IParameterModel> =
new (workspace: Workspace, name: string, id: string) => ParameterModel;
/** Serializes the given IProcedureModel to JSON. */
function saveProcedure(proc: IProcedureModel): State {
/**
* Serializes the given IProcedureModel to JSON.
*
* @internal
*/
export function saveProcedure(proc: IProcedureModel): State {
const state: State = {
id: proc.getId(),
name: proc.getName(),
@@ -62,8 +67,12 @@ function saveProcedure(proc: IProcedureModel): State {
return state;
}
/** Serializes the given IParameterModel to JSON. */
function saveParameter(param: IParameterModel): ParameterState {
/**
* Serializes the given IParameterModel to JSON.
*
* @internal
*/
export function saveParameter(param: IParameterModel): ParameterState {
const state: ParameterState = {
id: param.getId(),
name: param.getName(),
@@ -73,8 +82,12 @@ function saveParameter(param: IParameterModel): ParameterState {
return state;
}
/** Deserializes the given procedure model State from JSON. */
function
/**
* Deserializes the given procedure model State from JSON.
*
* @internal
*/
export function
loadProcedure<ProcedureModel extends IProcedureModel,
ParameterModel extends IParameterModel>(
procedureModelClass: ProcedureModelConstructor<ProcedureModel>,
@@ -90,12 +103,17 @@ loadProcedure<ProcedureModel extends IProcedureModel,
return proc;
}
/** Deserializes the given ParameterState from JSON. */
function loadParameter<ParameterModel extends IParameterModel>(
/**
* Deserializes the given ParameterState from JSON.
*
* @internal
*/
export function loadParameter<ParameterModel extends IParameterModel>(
parameterModelClass: ParameterModelConstructor<ParameterModel>,
state: ParameterState, workspace: Workspace): ParameterModel {
return new parameterModelClass(workspace, state.name, state.id)
.setTypes(state.types || []);
const model = new parameterModelClass(workspace, state.name, state.id);
if (state.types) model.setTypes(state.types);
return model;
}
/** Serializer for saving and loading procedure state. */

View File

@@ -229,13 +229,13 @@ export class Toolbox extends DeleteArea implements IAutoHideable,
container: HTMLDivElement, contentsContainer: HTMLDivElement) {
// Clicking on toolbox closes popups.
const clickEvent = browserEvents.conditionalBind(
container, 'click', this, this.onClick_,
/* opt_noCaptureIdentifier */ false, /* opt_noPreventDefault */ true);
container, 'pointerdown', this, this.onClick_,
/* opt_noCaptureIdentifier */ false);
this.boundEvents_.push(clickEvent);
const keyDownEvent = browserEvents.conditionalBind(
contentsContainer, 'keydown', this, this.onKeyDown_,
/* opt_noCaptureIdentifier */ false, /* opt_noPreventDefault */ true);
/* opt_noCaptureIdentifier */ false);
this.boundEvents_.push(keyDownEvent);
}
@@ -244,7 +244,7 @@ export class Toolbox extends DeleteArea implements IAutoHideable,
*
* @param e Click event to handle.
*/
protected onClick_(e: MouseEvent) {
protected onClick_(e: PointerEvent) {
if (browserEvents.isRightButton(e) || e.target === this.HtmlDiv) {
// Close flyout.
(common.getMainWorkspace() as WorkspaceSvg).hideChaff(false);

View File

@@ -231,14 +231,14 @@ export function createDom() {
export function bindMouseEvents(element: Element) {
// TODO (#6097): Don't stash wrapper info on the DOM.
(element as AnyDuringMigration).mouseOverWrapper_ =
browserEvents.bind(element, 'mouseover', null, onMouseOver);
browserEvents.bind(element, 'pointerover', null, onMouseOver);
(element as AnyDuringMigration).mouseOutWrapper_ =
browserEvents.bind(element, 'mouseout', null, onMouseOut);
browserEvents.bind(element, 'pointerout', null, onMouseOut);
// Don't use bindEvent_ for mousemove since that would create a
// corresponding touch handler, even though this only makes sense in the
// context of a mouseover/mouseout.
element.addEventListener('mousemove', onMouseMove, false);
element.addEventListener('pointermove', onMouseMove, false);
}
/**
@@ -254,7 +254,7 @@ export function unbindMouseEvents(element: Element|null) {
// TODO (#6097): Don't stash wrapper info on the DOM.
browserEvents.unbind((element as AnyDuringMigration).mouseOverWrapper_);
browserEvents.unbind((element as AnyDuringMigration).mouseOutWrapper_);
element.removeEventListener('mousemove', onMouseMove);
element.removeEventListener('pointermove', onMouseMove);
}
/**
@@ -263,7 +263,7 @@ export function unbindMouseEvents(element: Element|null) {
*
* @param e Mouse event.
*/
function onMouseOver(e: Event) {
function onMouseOver(e: PointerEvent) {
if (blocked) {
// Someone doesn't want us to show tooltips.
return;
@@ -285,7 +285,7 @@ function onMouseOver(e: Event) {
*
* @param _e Mouse event.
*/
function onMouseOut(_e: Event) {
function onMouseOut(_e: PointerEvent) {
if (blocked) {
// Someone doesn't want us to show tooltips.
return;

View File

@@ -13,6 +13,7 @@ import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Touch');
import type {Gesture} from './gesture.js';
import * as deprecation from './utils/deprecation.js';
/**
@@ -52,23 +53,17 @@ let touchIdentifier_: string|null = null;
*
* @alias Blockly.Touch.TOUCH_MAP
*/
export const TOUCH_MAP: {[key: string]: string[]} = globalThis['PointerEvent'] ?
{
'mousedown': ['pointerdown'],
'mouseenter': ['pointerenter'],
'mouseleave': ['pointerleave'],
'mousemove': ['pointermove'],
'mouseout': ['pointerout'],
'mouseover': ['pointerover'],
'mouseup': ['pointerup', 'pointercancel'],
'touchend': ['pointerup'],
'touchcancel': ['pointercancel'],
} :
{
'mousedown': ['touchstart'],
'mousemove': ['touchmove'],
'mouseup': ['touchend', 'touchcancel'],
};
export const TOUCH_MAP: {[key: string]: string[]} = {
'mousedown': ['pointerdown'],
'mouseenter': ['pointerenter'],
'mouseleave': ['pointerleave'],
'mousemove': ['pointermove'],
'mouseout': ['pointerout'],
'mouseover': ['pointerover'],
'mouseup': ['pointerup', 'pointercancel'],
'touchend': ['pointerup'],
'touchcancel': ['pointercancel'],
};
/** PID of queued long-press task. */
let longPid_: AnyDuringMigration = 0;
@@ -85,29 +80,9 @@ let longPid_: AnyDuringMigration = 0;
* @alias Blockly.Touch.longStart
* @internal
*/
export function longStart(e: Event, gesture: Gesture) {
export function longStart(e: PointerEvent, gesture: Gesture) {
longStop();
// Punt on multitouch events.
// AnyDuringMigration because: Property 'changedTouches' does not exist on
// type 'Event'.
if ((e as AnyDuringMigration).changedTouches &&
(e as AnyDuringMigration).changedTouches.length !== 1) {
return;
}
longPid_ = setTimeout(function() {
// TODO(#6097): Make types accurate, possibly by refactoring touch handling.
// AnyDuringMigration because: Property 'changedTouches' does not exist on
// type 'Event'.
const typelessEvent = e as AnyDuringMigration;
// Additional check to distinguish between touch events and pointer events
if (typelessEvent.changedTouches) {
// TouchEvent
typelessEvent.button = 2; // Simulate a right button click.
// e was a touch event. It needs to pretend to be a mouse event.
typelessEvent.clientX = typelessEvent.changedTouches[0].clientX;
typelessEvent.clientY = typelessEvent.changedTouches[0].clientY;
}
// Let the gesture route the right-click correctly.
if (gesture) {
gesture.handleRightClick(e);
@@ -150,78 +125,46 @@ export function clearTouchIdentifier() {
* handler; false if it should be blocked.
* @alias Blockly.Touch.shouldHandleEvent
*/
export function shouldHandleEvent(e: Event|PseudoEvent): boolean {
return !isMouseOrTouchEvent(e) || checkTouchIdentifier(e);
export function shouldHandleEvent(e: Event): boolean {
// Do not replace the startsWith with a check for `instanceof PointerEvent`.
// `click` and `contextmenu` are PointerEvents in some browsers,
// despite not starting with `pointer`, but we want to always handle them
// without worrying about touch identifiers.
return !(e.type.startsWith('pointer')) ||
(e instanceof PointerEvent && checkTouchIdentifier(e));
}
/**
* Get the touch identifier from the given event. If it was a mouse event, the
* identifier is the string 'mouse'.
* Get the pointer identifier from the given event.
*
* @param e Pointer event, mouse event, or touch event.
* @returns The pointerId, or touch identifier from the first changed touch, if
* defined. Otherwise 'mouse'.
* @param e Pointer event.
* @returns The pointerId of the event.
* @alias Blockly.Touch.getTouchIdentifierFromEvent
*/
export function getTouchIdentifierFromEvent(e: Event|PseudoEvent): string {
if (e instanceof PointerEvent) {
return String(e.pointerId);
}
if (e instanceof MouseEvent) {
return 'mouse';
}
/**
* TODO(#6097): Fix types. This is a catch-all for everything but mouse
* and pointer events.
*/
const pseudoEvent = /** {!PseudoEvent} */ e;
// AnyDuringMigration because: Property 'changedTouches' does not exist on
// type 'PseudoEvent | Event'. AnyDuringMigration because: Property
// 'changedTouches' does not exist on type 'PseudoEvent | Event'.
// AnyDuringMigration because: Property 'changedTouches' does not exist on
// type 'PseudoEvent | Event'. AnyDuringMigration because: Property
// 'changedTouches' does not exist on type 'PseudoEvent | Event'.
// AnyDuringMigration because: Property 'changedTouches' does not exist on
// type 'PseudoEvent | Event'.
return (pseudoEvent as AnyDuringMigration).changedTouches &&
(pseudoEvent as AnyDuringMigration).changedTouches[0] &&
(pseudoEvent as AnyDuringMigration).changedTouches[0].identifier !==
undefined &&
(pseudoEvent as AnyDuringMigration).changedTouches[0].identifier !==
null ?
String((pseudoEvent as AnyDuringMigration).changedTouches[0].identifier) :
'mouse';
export function getTouchIdentifierFromEvent(e: PointerEvent): string {
return `${e.pointerId}`;
}
/**
* Check whether the touch identifier on the event matches the current saved
* identifier. If there is no identifier, that means it's a mouse event and
* we'll use the identifier "mouse". This means we won't deal well with
* multiple mice being used at the same time. That seems okay.
* If the current identifier was unset, save the identifier from the
* event. This starts a drag/gesture, during which touch events with other
* identifiers will be silently ignored.
* Check whether the pointer identifier on the event matches the current saved
* identifier. If the current identifier was unset, save the identifier from
* the event. This starts a drag/gesture, during which pointer events with
* other identifiers will be silently ignored.
*
* @param e Mouse event or touch event.
* @param e Pointer event.
* @returns Whether the identifier on the event matches the current saved
* identifier.
* @alias Blockly.Touch.checkTouchIdentifier
*/
export function checkTouchIdentifier(e: Event|PseudoEvent): boolean {
export function checkTouchIdentifier(e: PointerEvent): boolean {
const identifier = getTouchIdentifierFromEvent(e);
// if (touchIdentifier_) is insufficient because Android touch
// identifiers may be zero.
if (touchIdentifier_ !== undefined && touchIdentifier_ !== null) {
if (touchIdentifier_) {
// We're already tracking some touch/mouse event. Is this from the same
// source?
return touchIdentifier_ === identifier;
}
if (e.type === 'mousedown' || e.type === 'touchstart' ||
e.type === 'pointerdown') {
if (e.type === 'pointerdown') {
// No identifier set yet, and this is the start of a drag. Set it and
// return.
touchIdentifier_ = identifier;
@@ -241,6 +184,7 @@ export function checkTouchIdentifier(e: Event|PseudoEvent): boolean {
* @alias Blockly.Touch.setClientFromTouch
*/
export function setClientFromTouch(e: Event|PseudoEvent) {
deprecation.warn('setClientFromTouch()', 'version 9', 'version 10');
// AnyDuringMigration because: Property 'changedTouches' does not exist on
// type 'PseudoEvent | Event'.
if (e.type.startsWith('touch') && (e as AnyDuringMigration).changedTouches) {
@@ -265,6 +209,7 @@ export function setClientFromTouch(e: Event|PseudoEvent) {
* @alias Blockly.Touch.isMouseOrTouchEvent
*/
export function isMouseOrTouchEvent(e: Event|PseudoEvent): boolean {
deprecation.warn('isMouseOrTouchEvent()', 'version 9', 'version 10');
return e.type.startsWith('touch') || e.type.startsWith('mouse') ||
e.type.startsWith('pointer');
}
@@ -277,6 +222,7 @@ export function isMouseOrTouchEvent(e: Event|PseudoEvent): boolean {
* @alias Blockly.Touch.isTouchEvent
*/
export function isTouchEvent(e: Event|PseudoEvent): boolean {
deprecation.warn('isTouchEvent()', 'version 9', 'version 10');
return e.type.startsWith('touch') || e.type.startsWith('pointer');
}
@@ -291,6 +237,7 @@ export function isTouchEvent(e: Event|PseudoEvent): boolean {
* @alias Blockly.Touch.splitEventByTouches
*/
export function splitEventByTouches(e: Event): Array<Event|PseudoEvent> {
deprecation.warn('splitEventByTouches()', 'version 9', 'version 10');
const events = [];
// AnyDuringMigration because: Property 'changedTouches' does not exist on
// type 'PseudoEvent | Event'.

View File

@@ -1,312 +0,0 @@
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* The class extends Gesture to support pinch to zoom
* for both pointer and touch events.
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.TouchGesture');
import * as browserEvents from './browser_events.js';
import {Gesture} from './gesture.js';
import * as Touch from './touch.js';
import {Coordinate} from './utils/coordinate.js';
/*
* Note: In this file "start" refers to touchstart, mousedown, and pointerstart
* events. "End" refers to touchend, mouseup, and pointerend events.
*/
/** A multiplier used to convert the gesture scale to a zoom in delta. */
const ZOOM_IN_MULTIPLIER = 5;
/** A multiplier used to convert the gesture scale to a zoom out delta. */
const ZOOM_OUT_MULTIPLIER = 6;
/**
* Class for one gesture.
*
* @alias Blockly.TouchGesture
*/
export class TouchGesture extends Gesture {
/** Boolean for whether or not this gesture is a multi-touch gesture. */
private isMultiTouch_ = false;
/** A map of cached points used for tracking multi-touch gestures. */
private cachedPoints = new Map<string, Coordinate|null>();
/**
* This is the ratio between the starting distance between the touch points
* and the most recent distance between the touch points.
* Scales between 0 and 1 mean the most recent zoom was a zoom out.
* Scales above 1.0 mean the most recent zoom was a zoom in.
*/
private previousScale_ = 0;
/** The starting distance between two touch points. */
private startDistance_ = 0;
/**
* A handle to use to unbind the second touch start or pointer down listener
* at the end of a drag.
* Opaque data returned from Blockly.bindEventWithChecks_.
*/
private onStartWrapper_: browserEvents.Data|null = null;
/** Boolean for whether or not the workspace supports pinch-zoom. */
private isPinchZoomEnabled_: boolean|null = null;
override onMoveWrapper_: browserEvents.Data|null = null;
override onUpWrapper_: browserEvents.Data|null = null;
/**
* Start a gesture: update the workspace to indicate that a gesture is in
* progress and bind mousemove and mouseup handlers.
*
* @param e A mouse down, touch start or pointer down event.
* @internal
*/
override doStart(e: MouseEvent) {
if (!this.startWorkspace_) {
throw new Error(
'Cannot start the touch event becauase the start ' +
'workspace is undefined');
}
this.isPinchZoomEnabled_ = this.startWorkspace_.options.zoomOptions &&
this.startWorkspace_.options.zoomOptions.pinch;
super.doStart(e);
if (!this.isEnding_ && Touch.isTouchEvent(e)) {
this.handleTouchStart(e);
}
}
/**
* Bind gesture events.
* Overriding the gesture definition of this function, binding the same
* functions for onMoveWrapper_ and onUpWrapper_ but passing
* opt_noCaptureIdentifier.
* In addition, binding a second mouse down event to detect multi-touch
* events.
*
* @param e A mouse down or touch start event.
* @internal
*/
override bindMouseEvents(e: Event) {
this.onStartWrapper_ = browserEvents.conditionalBind(
document, 'mousedown', null, this.handleStart.bind(this),
/* opt_noCaptureIdentifier */ true);
this.onMoveWrapper_ = browserEvents.conditionalBind(
document, 'mousemove', null, this.handleMove.bind(this),
/* opt_noCaptureIdentifier */ true);
this.onUpWrapper_ = browserEvents.conditionalBind(
document, 'mouseup', null, this.handleUp.bind(this),
/* opt_noCaptureIdentifier */ true);
e.preventDefault();
e.stopPropagation();
}
/**
* Handle a mouse down, touch start, or pointer down event.
*
* @param e A mouse down, touch start, or pointer down event.
* @internal
*/
handleStart(e: Event) {
if (this.isDragging()) {
// A drag has already started, so this can no longer be a pinch-zoom.
return;
}
if (Touch.isTouchEvent(e)) {
this.handleTouchStart(e);
if (this.isMultiTouch()) {
Touch.longStop();
}
}
}
/**
* Handle a mouse move, touch move, or pointer move event.
*
* @param e A mouse move, touch move, or pointer move event.
* @internal
*/
override handleMove(e: MouseEvent) {
if (this.isDragging()) {
// We are in the middle of a drag, only handle the relevant events
if (Touch.shouldHandleEvent(e)) {
super.handleMove(e);
}
return;
}
if (this.isMultiTouch()) {
if (Touch.isTouchEvent(e)) {
this.handleTouchMove(e);
}
Touch.longStop();
} else {
super.handleMove(e);
}
}
/**
* Handle a mouse up, touch end, or pointer up event.
*
* @param e A mouse up, touch end, or pointer up event.
* @internal
*/
override handleUp(e: Event) {
if (Touch.isTouchEvent(e) && !this.isDragging()) {
this.handleTouchEnd(e);
}
if (!this.isMultiTouch() || this.isDragging()) {
if (!Touch.shouldHandleEvent(e)) {
return;
}
super.handleUp(e);
} else {
e.preventDefault();
e.stopPropagation();
this.dispose();
}
}
/**
* Whether this gesture is part of a multi-touch gesture.
*
* @returns Whether this gesture is part of a multi-touch gesture.
* @internal
*/
isMultiTouch(): boolean {
return this.isMultiTouch_;
}
/**
* Sever all links from this object.
*
* @internal
*/
override dispose() {
super.dispose();
if (this.onStartWrapper_) {
browserEvents.unbind(this.onStartWrapper_);
}
}
/**
* Handle a touch start or pointer down event and keep track of current
* pointers.
*
* @param e A touch start, or pointer down event.
* @internal
*/
handleTouchStart(e: Event) {
const pointerId = Touch.getTouchIdentifierFromEvent(e);
// store the pointerId in the current list of pointers
this.cachedPoints.set(pointerId, this.getTouchPoint(e));
const pointers = Array.from(this.cachedPoints.keys());
// If two pointers are down, store info
if (pointers.length === 2) {
const point0 = (this.cachedPoints.get(pointers[0]))!;
const point1 = (this.cachedPoints.get(pointers[1]))!;
this.startDistance_ = Coordinate.distance(point0, point1);
this.isMultiTouch_ = true;
e.preventDefault();
}
}
/**
* Handle a touch move or pointer move event and zoom in/out if two pointers
* are on the screen.
*
* @param e A touch move, or pointer move event.
* @internal
*/
handleTouchMove(e: MouseEvent) {
const pointerId = Touch.getTouchIdentifierFromEvent(e);
// Update the cache
this.cachedPoints.set(pointerId, this.getTouchPoint(e));
if (this.isPinchZoomEnabled_ && this.cachedPoints.size === 2) {
this.handlePinch_(e);
} else {
super.handleMove(e);
}
}
/**
* Handle pinch zoom gesture.
*
* @param e A touch move, or pointer move event.
*/
private handlePinch_(e: MouseEvent) {
const pointers = Array.from(this.cachedPoints.keys());
// Calculate the distance between the two pointers
const point0 = (this.cachedPoints.get(pointers[0]))!;
const point1 = (this.cachedPoints.get(pointers[1]))!;
const moveDistance = Coordinate.distance(point0, point1);
const scale = moveDistance / this.startDistance_;
if (this.previousScale_ > 0 && this.previousScale_ < Infinity) {
const gestureScale = scale - this.previousScale_;
const delta = gestureScale > 0 ? gestureScale * ZOOM_IN_MULTIPLIER :
gestureScale * ZOOM_OUT_MULTIPLIER;
if (!this.startWorkspace_) {
throw new Error(
'Cannot handle a pinch because the start workspace ' +
'is undefined');
}
const workspace = this.startWorkspace_;
const position = browserEvents.mouseToSvg(
e, workspace.getParentSvg(), workspace.getInverseScreenCTM());
workspace.zoom(position.x, position.y, delta);
}
this.previousScale_ = scale;
e.preventDefault();
}
/**
* Handle a touch end or pointer end event and end the gesture.
*
* @param e A touch end, or pointer end event.
* @internal
*/
handleTouchEnd(e: Event) {
const pointerId = Touch.getTouchIdentifierFromEvent(e);
if (this.cachedPoints.has(pointerId)) {
this.cachedPoints.delete(pointerId);
}
if (this.cachedPoints.size < 2) {
this.cachedPoints.clear();
this.previousScale_ = 0;
}
}
/**
* Helper function returning the current touch point coordinate.
*
* @param e A touch or pointer event.
* @returns The current touch point coordinate
* @internal
*/
getTouchPoint(e: Event): Coordinate|null {
if (!this.startWorkspace_) {
return null;
}
// TODO(#6097): Make types accurate, possibly by refactoring touch handling.
const typelessEvent = e as AnyDuringMigration;
return new Coordinate(
typelessEvent.changedTouches ? typelessEvent.changedTouches[0].pageX :
typelessEvent.pageX,
typelessEvent.changedTouches ? typelessEvent.changedTouches[0].pageY :
typelessEvent.pageY);
}
}

View File

@@ -201,11 +201,11 @@ export class Trashcan extends DeleteArea implements IAutoHideable,
// Using bindEventWithChecks_ for blocking mousedown causes issue in mobile.
// See #4303
browserEvents.bind(
this.svgGroup_, 'mousedown', this, this.blockMouseDownWhenOpenable_);
browserEvents.bind(this.svgGroup_, 'mouseup', this, this.click);
this.svgGroup_, 'pointerdown', this, this.blockMouseDownWhenOpenable_);
browserEvents.bind(this.svgGroup_, 'pointerup', this, this.click);
// Bind to body instead of this.svgGroup_ so that we don't get lid jitters
browserEvents.bind(body, 'mouseover', this, this.mouseOver_);
browserEvents.bind(body, 'mouseout', this, this.mouseOut_);
browserEvents.bind(body, 'pointerover', this, this.mouseOver_);
browserEvents.bind(body, 'pointerout', this, this.mouseOut_);
this.animateLid_();
return this.svgGroup_;
}
@@ -275,7 +275,13 @@ export class Trashcan extends DeleteArea implements IAutoHideable,
const contents = this.contents_.map(function(string) {
return JSON.parse(string);
});
this.flyout?.show(contents);
// Trashcans with lots of blocks can take a second to render.
const blocklyStyle = this.workspace.getParentSvg().style;
blocklyStyle.cursor = 'wait';
setTimeout(() => {
this.flyout?.show(contents);
blocklyStyle.cursor = '';
}, 10);
this.fireUiEvent_(true);
}
@@ -513,7 +519,7 @@ export class Trashcan extends DeleteArea implements IAutoHideable,
*
* @param e A mouse down event.
*/
private blockMouseDownWhenOpenable_(e: Event) {
private blockMouseDownWhenOpenable_(e: PointerEvent) {
if (!this.contentsIsOpen() && this.hasContents_()) {
// Don't start a workspace scroll.
e.stopPropagation();

View File

@@ -59,6 +59,7 @@ export class VariableMap {
* @internal
*/
renameVariable(variable: VariableModel, newName: string) {
if (variable.name === newName) return;
const type = variable.type;
const conflictVar = this.getVariable(newName, type);
const blocks = this.workspace.getAllBlocks(false);
@@ -184,6 +185,8 @@ export class VariableMap {
this.variableMap.delete(type);
this.variableMap.set(type, variables);
eventUtils.fire(new (eventUtils.get(eventUtils.VAR_CREATE))(variable));
return variable;
}
/* Begin functions for variable deletion. */

View File

@@ -15,7 +15,6 @@ goog.declareModuleId('Blockly.VariableModel');
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_var_create.js';
import * as eventUtils from './events/utils.js';
import * as idGenerator from './utils/idgenerator.js';
import type {Workspace} from './workspace.js';
@@ -58,8 +57,6 @@ export class VariableModel {
* UUID.
*/
this.id_ = opt_id || idGenerator.genUid();
eventUtils.fire(new (eventUtils.get(eventUtils.VAR_CREATE))(this));
}
/** @returns The ID for the variable. */

View File

@@ -222,7 +222,7 @@ export function generateUniqueNameFromOptions(
}
}
if (!inUse) {
return potName;
break;
}
letterIndex++;
@@ -233,6 +233,7 @@ export function generateUniqueNameFromOptions(
}
potName = letters.charAt(letterIndex) + suffix;
}
return potName;
}
/**

View File

@@ -170,10 +170,10 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
}
if (!this.workspace.options.readOnly && !this.eventsInit_) {
browserEvents.conditionalBind(
this.svgRectTarget_ as SVGRectElement, 'mousedown', this,
this.svgRectTarget_ as SVGRectElement, 'pointerdown', this,
this.pathMouseDown_);
browserEvents.conditionalBind(
this.svgHandleTarget_ as SVGRectElement, 'mousedown', this,
this.svgHandleTarget_ as SVGRectElement, 'pointerdown', this,
this.pathMouseDown_);
}
this.eventsInit_ = true;
@@ -189,11 +189,11 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
}
/**
* Handle a mouse-down on an SVG comment.
* Handle a pointerdown on an SVG comment.
*
* @param e Mouse down event or touch start event.
* @param e Pointer down event.
*/
private pathMouseDown_(e: Event) {
private pathMouseDown_(e: PointerEvent) {
const gesture = this.workspace.getGesture(e);
if (gesture) {
gesture.handleBubbleStart(e, this);
@@ -203,11 +203,11 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
/**
* Show the context menu for this workspace comment.
*
* @param e Mouse event.
* @param e Pointer event.
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
showContextMenu(e: Event) {
showContextMenu(e: PointerEvent) {
throw new Error(
'The implementation of showContextMenu should be ' +
'monkey-patched in by blockly.ts');
@@ -685,18 +685,18 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
if (this.resizeGroup_) {
browserEvents.conditionalBind(
(this.resizeGroup_), 'mousedown', this, this.resizeMouseDown_);
(this.resizeGroup_), 'pointerdown', this, this.resizeMouseDown_);
}
if (this.isDeletable()) {
browserEvents.conditionalBind(
this.deleteGroup_ as SVGGElement, 'mousedown', this,
this.deleteGroup_ as SVGGElement, 'pointerdown', this,
this.deleteMouseDown_);
browserEvents.conditionalBind(
this.deleteGroup_ as SVGGElement, 'mouseout', this,
this.deleteGroup_ as SVGGElement, 'pointerout', this,
this.deleteMouseOut_);
browserEvents.conditionalBind(
this.deleteGroup_ as SVGGElement, 'mouseup', this,
this.deleteGroup_ as SVGGElement, 'pointerup', this,
this.deleteMouseUp_);
}
}
@@ -820,11 +820,11 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
}
/**
* Handle a mouse-down on comment's resize corner.
* Handle a pointerdown on comment's resize corner.
*
* @param e Mouse down event.
* @param e Pointer down event.
*/
private resizeMouseDown_(e: MouseEvent) {
private resizeMouseDown_(e: PointerEvent) {
this.unbindDragEvents_();
if (browserEvents.isRightButton(e)) {
// No right-click.
@@ -838,20 +838,20 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
this.workspace.RTL ? -this.width_ : this.width_, this.height_));
this.onMouseUpWrapper_ = browserEvents.conditionalBind(
document, 'mouseup', this, this.resizeMouseUp_);
document, 'pointerup', this, this.resizeMouseUp_);
this.onMouseMoveWrapper_ = browserEvents.conditionalBind(
document, 'mousemove', this, this.resizeMouseMove_);
document, 'pointermove', this, this.resizeMouseMove_);
this.workspace.hideChaff();
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
}
/**
* Handle a mouse-down on comment's delete icon.
* Handle a pointerdown on comment's delete icon.
*
* @param e Mouse down event.
* @param e Pointer down event.
*/
private deleteMouseDown_(e: Event) {
private deleteMouseDown_(e: PointerEvent) {
// Highlight the delete icon.
if (this.deleteIconBorder_) {
dom.addClass(this.deleteIconBorder_, 'blocklyDeleteIconHighlighted');
@@ -861,11 +861,11 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
}
/**
* Handle a mouse-out on comment's delete icon.
* Handle a pointerout on comment's delete icon.
*
* @param _e Mouse out event.
* @param _e Pointer out event.
*/
private deleteMouseOut_(_e: Event) {
private deleteMouseOut_(_e: PointerEvent) {
// Restore highlight on the delete icon.
if (this.deleteIconBorder_) {
dom.removeClass(this.deleteIconBorder_, 'blocklyDeleteIconHighlighted');
@@ -873,18 +873,18 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
}
/**
* Handle a mouse-up on comment's delete icon.
* Handle a pointerup on comment's delete icon.
*
* @param e Mouse up event.
* @param e Pointer up event.
*/
private deleteMouseUp_(e: Event) {
private deleteMouseUp_(e: PointerEvent) {
// Delete this comment.
this.dispose();
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
}
/** Stop binding to the global mouseup and mousemove events. */
/** Stop binding to the global pointerup and pointermove events. */
private unbindDragEvents_() {
if (this.onMouseUpWrapper_) {
browserEvents.unbind(this.onMouseUpWrapper_);
@@ -897,21 +897,22 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
}
/**
* Handle a mouse-up event while dragging a comment's border or resize handle.
* Handle a pointerup event while dragging a comment's border or resize
* handle.
*
* @param _e Mouse up event.
* @param _e Pointer up event.
*/
private resizeMouseUp_(_e: Event) {
private resizeMouseUp_(_e: PointerEvent) {
Touch.clearTouchIdentifier();
this.unbindDragEvents_();
}
/**
* Resize this comment to follow the mouse.
* Resize this comment to follow the pointer.
*
* @param e Mouse move event.
* @param e Pointer move event.
*/
private resizeMouseMove_(e: MouseEvent) {
private resizeMouseMove_(e: PointerEvent) {
this.autoLayout_ = false;
const newXY = this.workspace.moveDrag(e);
this.setSize_(this.RTL ? -newXY.x : newXY.x, newXY.y);

View File

@@ -56,7 +56,6 @@ import type {Theme} from './theme.js';
import {Classic} from './theme/classic.js';
import {ThemeManager} from './theme_manager.js';
import * as Tooltip from './tooltip.js';
import {TouchGesture} from './touch_gesture.js';
import type {Trashcan} from './trashcan.js';
import * as arrayUtils from './utils/array.js';
import {Coordinate} from './utils/coordinate.js';
@@ -224,7 +223,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
*
* @internal
*/
currentGesture_: TouchGesture|null = null;
currentGesture_: Gesture|null = null;
/** This workspace's surface for dragging blocks, if it exists. */
private readonly blockDragSurface: BlockDragSurfaceSvg|null = null;
@@ -774,7 +773,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
if (!this.isFlyout) {
browserEvents.conditionalBind(
this.svgGroup_, 'mousedown', this, this.onMouseDown_, false, true);
this.svgGroup_, 'pointerdown', this, this.onMouseDown_, false);
// This no-op works around https://bugs.webkit.org/show_bug.cgi?id=226683,
// which otherwise prevents zoom/scroll events from being observed in
// Safari. Once that bug is fixed it should be removed.
@@ -959,7 +958,6 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
*
* @param opt_own Whether to only return the workspace's own flyout.
* @returns The flyout on this workspace.
* @internal
*/
getFlyout(opt_own?: boolean): IFlyout|null {
if (this.flyout || opt_own) {
@@ -975,7 +973,6 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
* Getter for the toolbox associated with this workspace, if one exists.
*
* @returns The toolbox on this workspace.
* @internal
*/
getToolbox(): IToolbox|null {
return this.toolbox_;
@@ -1596,17 +1593,15 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
/* eslint-enable */
/**
* Returns the drag target the mouse event is over.
* Returns the drag target the pointer event is over.
*
* @param e Mouse move event.
* @param e Pointer move event.
* @returns Null if not over a drag target, or the drag target the event is
* over.
*/
getDragTarget(e: Event): IDragTarget|null {
getDragTarget(e: PointerEvent): IDragTarget|null {
for (let i = 0, targetArea; targetArea = this.dragTargetAreas[i]; i++) {
if (targetArea.clientRect.contains(
(e as AnyDuringMigration).clientX,
(e as AnyDuringMigration).clientY)) {
if (targetArea.clientRect.contains(e.clientX, e.clientY)) {
return targetArea.component;
}
}
@@ -1614,11 +1609,11 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
}
/**
* Handle a mouse-down on SVG drawing surface.
* Handle a pointerdown on SVG drawing surface.
*
* @param e Mouse down event.
* @param e Pointer down event.
*/
private onMouseDown_(e: MouseEvent) {
private onMouseDown_(e: PointerEvent) {
const gesture = this.getGesture(e);
if (gesture) {
gesture.handleWsStart(e, this);
@@ -1628,10 +1623,10 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
/**
* Start tracking a drag of an object on this workspace.
*
* @param e Mouse down event.
* @param e Pointer down event.
* @param xy Starting location of object.
*/
startDrag(e: MouseEvent, xy: Coordinate) {
startDrag(e: PointerEvent, xy: Coordinate) {
// Record the starting offset between the bubble's location and the mouse.
const point = browserEvents.mouseToSvg(
e, this.getParentSvg(), this.getInverseScreenCTM());
@@ -1644,10 +1639,10 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
/**
* Track a drag of an object on this workspace.
*
* @param e Mouse move event.
* @param e Pointer move event.
* @returns New location of object.
*/
moveDrag(e: MouseEvent): Coordinate {
moveDrag(e: PointerEvent): Coordinate {
const point = browserEvents.mouseToSvg(
e, this.getParentSvg(), this.getInverseScreenCTM());
// Fix scale of mouse event.
@@ -2473,14 +2468,13 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
* Look up the gesture that is tracking this touch stream on this workspace.
* May create a new gesture.
*
* @param e Mouse event or touch event.
* @param e Pointer event.
* @returns The gesture that is tracking this touch stream, or null if no
* valid gesture exists.
* @internal
*/
getGesture(e: Event): TouchGesture|null {
const isStart = e.type === 'mousedown' || e.type === 'touchstart' ||
e.type === 'pointerdown';
getGesture(e: PointerEvent): Gesture|null {
const isStart = e.type === 'pointerdown';
const gesture = this.currentGesture_;
if (gesture) {
@@ -2497,7 +2491,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
// No gesture existed on this workspace, but this looks like the start of a
// new gesture.
if (isStart) {
this.currentGesture_ = new TouchGesture(e, this);
this.currentGesture_ = new Gesture(e, this);
return this.currentGesture_;
}
// No gesture existed and this event couldn't be the start of a new gesture.

View File

@@ -276,7 +276,7 @@ export class ZoomControls implements IPositionable {
// Attach listener.
this.onZoomOutWrapper = browserEvents.conditionalBind(
this.zoomOutGroup, 'mousedown', null, this.zoom.bind(this, -1));
this.zoomOutGroup, 'pointerdown', null, this.zoom.bind(this, -1));
}
/**
@@ -322,7 +322,7 @@ export class ZoomControls implements IPositionable {
// Attach listener.
this.onZoomInWrapper = browserEvents.conditionalBind(
this.zoomInGroup, 'mousedown', null, this.zoom.bind(this, 1));
this.zoomInGroup, 'pointerdown', null, this.zoom.bind(this, 1));
}
/**
@@ -333,7 +333,7 @@ export class ZoomControls implements IPositionable {
* positive amount values zoom in.
* @param e A mouse down event.
*/
private zoom(amount: number, e: Event) {
private zoom(amount: number, e: PointerEvent) {
this.workspace.markFocused();
this.workspace.zoomCenter(amount);
this.fireZoomEvent();
@@ -380,7 +380,7 @@ export class ZoomControls implements IPositionable {
// Attach event listeners.
this.onZoomResetWrapper = browserEvents.conditionalBind(
this.zoomResetGroup, 'mousedown', null, this.resetZoom.bind(this));
this.zoomResetGroup, 'pointerdown', null, this.resetZoom.bind(this));
}
/**
@@ -388,7 +388,7 @@ export class ZoomControls implements IPositionable {
*
* @param e A mouse down event.
*/
private resetZoom(e: Event) {
private resetZoom(e: PointerEvent) {
this.workspace.markFocused();
// zoom is passed amount and computes the new scale using the formula:

View File

@@ -41,7 +41,6 @@ module.exports = {
gitUpdateGithubPages: gitTasks.updateGithubPages,
// Manually-invokable targets, with prequisites where required.
prepare: buildTasks.prepare,
format: buildTasks.format,
messages: buildTasks.messages, // Generate msg/json/en.json et al.
sortRequires: cleanupTasks.sortRequires,
@@ -52,9 +51,6 @@ module.exports = {
buildAdvancedCompilationTest: buildTasks.buildAdvancedCompilationTest,
gitCreateRC: gitTasks.createRC,
docs: docsTasks.docs,
// Targets intended only for invocation by scripts; may omit prerequisites.
onlyBuildAdvancedCompilationTest: buildTasks.onlyBuildAdvancedCompilationTest,
// Legacy targets, to be deleted.
recompile: releaseTasks.recompile,

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