mirror of
https://github.com/google/blockly.git
synced 2026-01-12 19:37:08 +01:00
release: Merge branch 'develop' into rc/v9.2.0
This commit is contained in:
@@ -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/*
|
||||
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -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
16
.github/release.yml
vendored
@@ -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:
|
||||
- "*"
|
||||
|
||||
2
.github/workflows/appengine_deploy.yml
vendored
2
.github/workflows/appengine_deploy.yml
vendored
@@ -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:
|
||||
|
||||
4
.github/workflows/check_clang_format.yml
vendored
4
.github/workflows/check_clang_format.yml
vendored
@@ -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.
|
||||
|
||||
8
.github/workflows/conventional-label.yml
vendored
8
.github/workflows/conventional-label.yml
vendored
@@ -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: '[]'
|
||||
|
||||
2
.github/workflows/update_metadata.yml
vendored
2
.github/workflows/update_metadata.yml
vendored
@@ -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
|
||||
|
||||
11
appengine/blockly_compressed.js
Normal file
11
appengine/blockly_compressed.js
Normal 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.
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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']) {
|
||||
|
||||
@@ -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_ = [];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface BlocklyOptions {
|
||||
maxBlocks?: number;
|
||||
maxInstances?: {[blockType: string]: number};
|
||||
media?: string;
|
||||
modalInputs?: boolean;
|
||||
move?: MoveOptions;
|
||||
oneBasedIndex?: boolean;
|
||||
readOnly?: boolean;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
*
|
||||
|
||||
@@ -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?
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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?
|
||||
*
|
||||
|
||||
@@ -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 {
|
||||
|
||||
37
core/events/events_procedure_base.ts
Normal file
37
core/events/events_procedure_base.ts
Normal 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;
|
||||
}
|
||||
87
core/events/events_procedure_change_return.ts
Normal file
87
core/events/events_procedure_change_return.ts
Normal 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);
|
||||
74
core/events/events_procedure_create.ts
Normal file
74
core/events/events_procedure_create.ts
Normal 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);
|
||||
71
core/events/events_procedure_delete.ts
Normal file
71
core/events/events_procedure_delete.ts
Normal 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);
|
||||
76
core/events/events_procedure_enable.ts
Normal file
76
core/events/events_procedure_enable.ts
Normal 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);
|
||||
39
core/events/events_procedure_parameter_base.ts
Normal file
39
core/events/events_procedure_parameter_base.ts
Normal 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,
|
||||
}
|
||||
101
core/events/events_procedure_parameter_create.ts
Normal file
101
core/events/events_procedure_parameter_create.ts
Normal 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);
|
||||
97
core/events/events_procedure_parameter_delete.ts
Normal file
97
core/events/events_procedure_parameter_delete.ts
Normal 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);
|
||||
102
core/events/events_procedure_parameter_rename.ts
Normal file
102
core/events/events_procedure_parameter_rename.ts
Normal 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);
|
||||
78
core/events/events_procedure_rename.ts
Normal file
78
core/events/events_procedure_rename.ts
Normal 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);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
593
core/field_input.ts
Normal 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;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
467
core/gesture.ts
467
core/gesture.ts
@@ -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
|
||||
|
||||
10
core/icon.ts
10
core/icon.ts
@@ -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_) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
25
core/interfaces/i_observable.ts
Normal file
25
core/interfaces/i_observable.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
26
core/menu.ts
26
core/menu.ts
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
127
core/touch.ts
127
core/touch.ts
@@ -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'.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user