mirror of
https://github.com/google/blockly.git
synced 2026-01-11 10:57:07 +01:00
release: Merge branch 'rc/v11.0.0' into develop
release: Merge branch 'rc/v11.0.0' into develop
This commit is contained in:
@@ -6,7 +6,6 @@
|
||||
|
||||
// Former goog.module ID: Blockly.libraryBlocks
|
||||
|
||||
import * as colour from './colour.js';
|
||||
import * as lists from './lists.js';
|
||||
import * as logic from './logic.js';
|
||||
import * as loops from './loops.js';
|
||||
@@ -18,7 +17,6 @@ import * as variablesDynamic from './variables_dynamic.js';
|
||||
import type {BlockDefinition} from '../core/blocks.js';
|
||||
|
||||
export {
|
||||
colour,
|
||||
lists,
|
||||
logic,
|
||||
loops,
|
||||
@@ -35,7 +33,6 @@ export {
|
||||
*/
|
||||
export const blocks: {[key: string]: BlockDefinition} = Object.assign(
|
||||
{},
|
||||
colour.blocks,
|
||||
lists.blocks,
|
||||
logic.blocks,
|
||||
loops.blocks,
|
||||
|
||||
112
blocks/colour.ts
112
blocks/colour.ts
@@ -1,112 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2012 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Former goog.module ID: Blockly.libraryBlocks.colour
|
||||
|
||||
import {
|
||||
createBlockDefinitionsFromJsonArray,
|
||||
defineBlocks,
|
||||
} from '../core/common.js';
|
||||
import '../core/field_colour.js';
|
||||
|
||||
/**
|
||||
* A dictionary of the block definitions provided by this module.
|
||||
*/
|
||||
export const blocks = createBlockDefinitionsFromJsonArray([
|
||||
// Block for colour picker.
|
||||
{
|
||||
'type': 'colour_picker',
|
||||
'message0': '%1',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_colour',
|
||||
'name': 'COLOUR',
|
||||
'colour': '#ff0000',
|
||||
},
|
||||
],
|
||||
'output': 'Colour',
|
||||
'helpUrl': '%{BKY_COLOUR_PICKER_HELPURL}',
|
||||
'style': 'colour_blocks',
|
||||
'tooltip': '%{BKY_COLOUR_PICKER_TOOLTIP}',
|
||||
'extensions': ['parent_tooltip_when_inline'],
|
||||
},
|
||||
|
||||
// Block for random colour.
|
||||
{
|
||||
'type': 'colour_random',
|
||||
'message0': '%{BKY_COLOUR_RANDOM_TITLE}',
|
||||
'output': 'Colour',
|
||||
'helpUrl': '%{BKY_COLOUR_RANDOM_HELPURL}',
|
||||
'style': 'colour_blocks',
|
||||
'tooltip': '%{BKY_COLOUR_RANDOM_TOOLTIP}',
|
||||
},
|
||||
|
||||
// Block for composing a colour from RGB components.
|
||||
{
|
||||
'type': 'colour_rgb',
|
||||
'message0':
|
||||
'%{BKY_COLOUR_RGB_TITLE} %{BKY_COLOUR_RGB_RED} %1 %{BKY_COLOUR_RGB_GREEN} %2 %{BKY_COLOUR_RGB_BLUE} %3',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'RED',
|
||||
'check': 'Number',
|
||||
'align': 'RIGHT',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'GREEN',
|
||||
'check': 'Number',
|
||||
'align': 'RIGHT',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'BLUE',
|
||||
'check': 'Number',
|
||||
'align': 'RIGHT',
|
||||
},
|
||||
],
|
||||
'output': 'Colour',
|
||||
'helpUrl': '%{BKY_COLOUR_RGB_HELPURL}',
|
||||
'style': 'colour_blocks',
|
||||
'tooltip': '%{BKY_COLOUR_RGB_TOOLTIP}',
|
||||
},
|
||||
|
||||
// Block for blending two colours together.
|
||||
{
|
||||
'type': 'colour_blend',
|
||||
'message0':
|
||||
'%{BKY_COLOUR_BLEND_TITLE} %{BKY_COLOUR_BLEND_COLOUR1} ' +
|
||||
'%1 %{BKY_COLOUR_BLEND_COLOUR2} %2 %{BKY_COLOUR_BLEND_RATIO} %3',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'COLOUR1',
|
||||
'check': 'Colour',
|
||||
'align': 'RIGHT',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'COLOUR2',
|
||||
'check': 'Colour',
|
||||
'align': 'RIGHT',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'RATIO',
|
||||
'check': 'Number',
|
||||
'align': 'RIGHT',
|
||||
},
|
||||
],
|
||||
'output': 'Colour',
|
||||
'helpUrl': '%{BKY_COLOUR_BLEND_HELPURL}',
|
||||
'style': 'colour_blocks',
|
||||
'tooltip': '%{BKY_COLOUR_BLEND_TOOLTIP}',
|
||||
},
|
||||
]);
|
||||
|
||||
// Register provided blocks.
|
||||
defineBlocks(blocks);
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
createBlockDefinitionsFromJsonArray,
|
||||
defineBlocks,
|
||||
} from '../core/common.js';
|
||||
import * as eventUtils from '../core/events/utils.js';
|
||||
import '../core/field_dropdown.js';
|
||||
import '../core/field_label.js';
|
||||
import '../core/field_number.js';
|
||||
@@ -334,6 +335,11 @@ export type ControlFlowInLoopBlock = Block & ControlFlowInLoopMixin;
|
||||
interface ControlFlowInLoopMixin extends ControlFlowInLoopMixinType {}
|
||||
type ControlFlowInLoopMixinType = typeof CONTROL_FLOW_IN_LOOP_CHECK_MIXIN;
|
||||
|
||||
/**
|
||||
* The language-neutral ID for when the reason why a block is disabled is
|
||||
* because the block is only valid inside of a loop.
|
||||
*/
|
||||
const CONTROL_FLOW_NOT_IN_LOOP_DISABLED_REASON = 'CONTROL_FLOW_NOT_IN_LOOP';
|
||||
/**
|
||||
* This mixin adds a check to make sure the 'controls_flow_statements' block
|
||||
* is contained in a loop. Otherwise a warning is added to the block.
|
||||
@@ -365,19 +371,30 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
|
||||
// Don't change state if:
|
||||
// * It's at the start of a drag.
|
||||
// * It's not a move event.
|
||||
if (!ws.isDragging || ws.isDragging() || e.type !== Events.BLOCK_MOVE) {
|
||||
if (
|
||||
!ws.isDragging ||
|
||||
ws.isDragging() ||
|
||||
(e.type !== Events.BLOCK_MOVE && e.type !== Events.BLOCK_CREATE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const enabled = !!this.getSurroundLoop();
|
||||
this.setWarningText(
|
||||
enabled ? null : Msg['CONTROLS_FLOW_STATEMENTS_WARNING'],
|
||||
);
|
||||
|
||||
if (!this.isInFlyout) {
|
||||
const group = Events.getGroup();
|
||||
// Makes it so the move and the disable event get undone together.
|
||||
Events.setGroup(e.group);
|
||||
this.setEnabled(enabled);
|
||||
Events.setGroup(group);
|
||||
try {
|
||||
// There is no need to record the enable/disable change on the undo/redo
|
||||
// list since the change will be automatically recreated when replayed.
|
||||
eventUtils.setRecordUndo(false);
|
||||
this.setDisabledReason(
|
||||
!enabled,
|
||||
CONTROL_FLOW_NOT_IN_LOOP_DISABLED_REASON,
|
||||
);
|
||||
} finally {
|
||||
eventUtils.setRecordUndo(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import * as Xml from '../core/xml.js';
|
||||
import * as fieldRegistry from '../core/field_registry.js';
|
||||
import * as xmlUtils from '../core/utils/xml.js';
|
||||
import type {Abstract as AbstractEvent} from '../core/events/events_abstract.js';
|
||||
import {Align} from '../core/inputs/input.js';
|
||||
import {Align} from '../core/inputs/align.js';
|
||||
import type {Block} from '../core/block.js';
|
||||
import type {BlockSvg} from '../core/block_svg.js';
|
||||
import type {BlockCreate} from '../core/events/events_block_create.js';
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
ContextMenuOption,
|
||||
LegacyContextMenuOption,
|
||||
} from '../core/contextmenu_registry.js';
|
||||
import * as eventUtils from '../core/events/utils.js';
|
||||
import {FieldCheckbox} from '../core/field_checkbox.js';
|
||||
import {FieldLabel} from '../core/field_label.js';
|
||||
import {FieldTextInput} from '../core/field_textinput.js';
|
||||
@@ -38,6 +39,7 @@ import {config} from '../core/config.js';
|
||||
import {defineBlocks} from '../core/common.js';
|
||||
import '../core/icons/comment_icon.js';
|
||||
import '../core/icons/warning_icon.js';
|
||||
import * as common from '../core/common.js';
|
||||
|
||||
/** A dictionary of the block definitions provided by this module. */
|
||||
export const blocks: {[key: string]: BlockDefinition} = {};
|
||||
@@ -753,7 +755,6 @@ interface CallMixin extends CallMixinType {
|
||||
defType_: string;
|
||||
quarkIds_: string[] | null;
|
||||
quarkConnections_: {[id: string]: Connection};
|
||||
previousEnabledState_: boolean;
|
||||
}
|
||||
type CallMixinType = typeof PROCEDURE_CALL_COMMON;
|
||||
|
||||
@@ -763,6 +764,13 @@ type CallExtraState = {
|
||||
params?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The language-neutral ID for when the reason why a block is disabled is
|
||||
* because the block's corresponding procedure definition is disabled.
|
||||
*/
|
||||
const DISABLED_PROCEDURE_DEFINITION_DISABLED_REASON =
|
||||
'DISABLED_PROCEDURE_DEFINITION';
|
||||
|
||||
/**
|
||||
* Common properties for the procedure_callnoreturn and
|
||||
* procedure_callreturn blocks.
|
||||
@@ -921,10 +929,9 @@ const PROCEDURE_CALL_COMMON = {
|
||||
type: 'field_label',
|
||||
text: this.arguments_[i],
|
||||
}) as FieldLabel;
|
||||
const input = this.appendValueInput('ARG' + i)
|
||||
this.appendValueInput('ARG' + i)
|
||||
.setAlign(Align.RIGHT)
|
||||
.appendField(newField, 'ARGNAME' + i);
|
||||
input.init();
|
||||
}
|
||||
}
|
||||
// Remove deleted inputs.
|
||||
@@ -937,7 +944,6 @@ const PROCEDURE_CALL_COMMON = {
|
||||
if (this.arguments_.length) {
|
||||
if (!this.getField('WITH')) {
|
||||
topRow.appendField(Msg['PROCEDURES_CALL_BEFORE_PARAMS'], 'WITH');
|
||||
topRow.init();
|
||||
}
|
||||
} else {
|
||||
if (this.getField('WITH')) {
|
||||
@@ -1125,12 +1131,16 @@ const PROCEDURE_CALL_COMMON = {
|
||||
);
|
||||
}
|
||||
Events.setGroup(event.group);
|
||||
if (blockChangeEvent.newValue) {
|
||||
this.previousEnabledState_ = this.isEnabled();
|
||||
this.setEnabled(false);
|
||||
} else {
|
||||
this.setEnabled(this.previousEnabledState_);
|
||||
}
|
||||
const valid = def.isEnabled();
|
||||
this.setDisabledReason(
|
||||
!valid,
|
||||
DISABLED_PROCEDURE_DEFINITION_DISABLED_REASON,
|
||||
);
|
||||
this.setWarningText(
|
||||
valid
|
||||
? null
|
||||
: Msg['PROCEDURES_CALL_DISABLED_DEF_WARNING'].replace('%1', name),
|
||||
);
|
||||
Events.setGroup(oldGroup);
|
||||
}
|
||||
}
|
||||
@@ -1159,7 +1169,7 @@ const PROCEDURE_CALL_COMMON = {
|
||||
const def = Procedures.getDefinition(name, workspace);
|
||||
if (def) {
|
||||
(workspace as WorkspaceSvg).centerOnBlock(def.id);
|
||||
(def as BlockSvg).select();
|
||||
common.setSelected(def as BlockSvg);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1182,7 +1192,6 @@ blocks['procedures_callnoreturn'] = {
|
||||
this.argumentVarModels_ = [];
|
||||
this.quarkConnections_ = {};
|
||||
this.quarkIds_ = null;
|
||||
this.previousEnabledState_ = true;
|
||||
},
|
||||
|
||||
defType_: 'procedures_defnoreturn',
|
||||
@@ -1203,7 +1212,6 @@ blocks['procedures_callreturn'] = {
|
||||
this.argumentVarModels_ = [];
|
||||
this.quarkConnections_ = {};
|
||||
this.quarkIds_ = null;
|
||||
this.previousEnabledState_ = true;
|
||||
},
|
||||
|
||||
defType_: 'procedures_defreturn',
|
||||
@@ -1220,6 +1228,12 @@ interface IfReturnMixin extends IfReturnMixinType {
|
||||
}
|
||||
type IfReturnMixinType = typeof PROCEDURES_IFRETURN;
|
||||
|
||||
/**
|
||||
* The language-neutral ID for when the reason why a block is disabled is
|
||||
* because the block is only valid inside of a procedure body.
|
||||
*/
|
||||
const UNPARENTED_IFRETURN_DISABLED_REASON = 'UNPARENTED_IFRETURN';
|
||||
|
||||
const PROCEDURES_IFRETURN = {
|
||||
/**
|
||||
* Block for conditionally returning a value from a procedure.
|
||||
@@ -1280,7 +1294,7 @@ const PROCEDURES_IFRETURN = {
|
||||
if (
|
||||
((this.workspace as WorkspaceSvg).isDragging &&
|
||||
(this.workspace as WorkspaceSvg).isDragging()) ||
|
||||
e.type !== Events.BLOCK_MOVE
|
||||
(e.type !== Events.BLOCK_MOVE && e.type !== Events.BLOCK_CREATE)
|
||||
) {
|
||||
return; // Don't change state at the start of a drag.
|
||||
}
|
||||
@@ -1316,12 +1330,16 @@ const PROCEDURES_IFRETURN = {
|
||||
} else {
|
||||
this.setWarningText(Msg['PROCEDURES_IFRETURN_WARNING']);
|
||||
}
|
||||
|
||||
if (!this.isInFlyout) {
|
||||
const group = Events.getGroup();
|
||||
// Makes it so the move and the disable event get undone together.
|
||||
Events.setGroup(e.group);
|
||||
this.setEnabled(legal);
|
||||
Events.setGroup(group);
|
||||
try {
|
||||
// There is no need to record the enable/disable change on the undo/redo
|
||||
// list since the change will be automatically recreated when replayed.
|
||||
eventUtils.setRecordUndo(false);
|
||||
this.setDisabledReason(!legal, UNPARENTED_IFRETURN_DISABLED_REASON);
|
||||
} finally {
|
||||
eventUtils.setRecordUndo(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
createBlockDefinitionsFromJsonArray,
|
||||
defineBlocks,
|
||||
} from '../core/common.js';
|
||||
import '../core/field_multilineinput.js';
|
||||
import '../core/field_variable.js';
|
||||
import {ValueInput} from '../core/inputs/value_input.js';
|
||||
|
||||
@@ -48,38 +47,6 @@ export const blocks = createBlockDefinitionsFromJsonArray([
|
||||
'tooltip': '%{BKY_TEXT_TEXT_TOOLTIP}',
|
||||
'extensions': ['text_quotes', 'parent_tooltip_when_inline'],
|
||||
},
|
||||
{
|
||||
'type': 'text_multiline',
|
||||
'message0': '%1 %2',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_image',
|
||||
'src':
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAARCAYAAADpP' +
|
||||
'U2iAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAdhgAAHYYBXaITgQAAABh0RVh0' +
|
||||
'U29mdHdhcmUAcGFpbnQubmV0IDQuMS42/U4J6AAAAP1JREFUOE+Vks0KQUEYhjm' +
|
||||
'RIja4ABtZ2dm5A3t3Ia6AUm7CylYuQRaUhZSlLZJiQbFAyRnPN33y01HOW08z88' +
|
||||
'73zpwzM4F3GWOCruvGIE4/rLaV+Nq1hVGMBqzhqlxgCys4wJA65xnogMHsQ5luj' +
|
||||
'nYHTejBBCK2mE4abjCgMGhNxHgDFWjDSG07kdfVa2pZMf4ZyMAdWmpZMfYOsLiD' +
|
||||
'MYMjlMB+K613QISRhTnITnsYg5yUd0DETmEoMlkFOeIT/A58iyK5E18BuTBfgYX' +
|
||||
'fwNJv4P9/oEBerLylOnRhygmGdPpTTBZAPkde61lbQe4moWUvYUZYLfUNftIY4z' +
|
||||
'wA5X2Z9AYnQrEAAAAASUVORK5CYII=',
|
||||
'width': 12,
|
||||
'height': 17,
|
||||
'alt': '\u00B6',
|
||||
},
|
||||
{
|
||||
'type': 'field_multilinetext',
|
||||
'name': 'TEXT',
|
||||
'text': '',
|
||||
},
|
||||
],
|
||||
'output': 'String',
|
||||
'style': 'text_blocks',
|
||||
'helpUrl': '%{BKY_TEXT_TEXT_HELPURL}',
|
||||
'tooltip': '%{BKY_TEXT_TEXT_TOOLTIP}',
|
||||
'extensions': ['parent_tooltip_when_inline'],
|
||||
},
|
||||
{
|
||||
'type': 'text_join',
|
||||
'message0': '',
|
||||
|
||||
177
core/block.ts
177
core/block.ts
@@ -25,7 +25,9 @@ import {ConnectionType} from './connection_type.js';
|
||||
import * as constants from './constants.js';
|
||||
import {DuplicateIconType} from './icons/exceptions.js';
|
||||
import type {Abstract} from './events/events_abstract.js';
|
||||
import type {BlockChange} from './events/events_block_change.js';
|
||||
import type {BlockMove} from './events/events_block_move.js';
|
||||
import * as deprecation from './utils/deprecation.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import * as Extensions from './extensions.js';
|
||||
import type {Field} from './field.js';
|
||||
@@ -33,9 +35,8 @@ import * as fieldRegistry from './field_registry.js';
|
||||
import {Input} from './inputs/input.js';
|
||||
import {Align} from './inputs/align.js';
|
||||
import type {IASTNodeLocation} from './interfaces/i_ast_node_location.js';
|
||||
import type {IDeletable} from './interfaces/i_deletable.js';
|
||||
import type {IIcon} from './interfaces/i_icon.js';
|
||||
import {CommentIcon} from './icons/comment_icon.js';
|
||||
import {type IIcon} from './interfaces/i_icon.js';
|
||||
import {isCommentIcon} from './interfaces/i_comment_icon.js';
|
||||
import type {MutatorIcon} from './icons/mutator_icon.js';
|
||||
import * as Tooltip from './tooltip.js';
|
||||
import * as arrayUtils from './utils/array.js';
|
||||
@@ -56,7 +57,7 @@ import {IconType} from './icons/icon_types.js';
|
||||
* Class for one block.
|
||||
* Not normally called directly, workspace.newBlock() is preferred.
|
||||
*/
|
||||
export class Block implements IASTNodeLocation, IDeletable {
|
||||
export class Block implements IASTNodeLocation {
|
||||
/**
|
||||
* An optional callback method to use whenever the block's parent workspace
|
||||
* changes. This is usually only called from the constructor, the block type
|
||||
@@ -167,7 +168,7 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
inputList: Input[] = [];
|
||||
inputsInline?: boolean;
|
||||
icons: IIcon[] = [];
|
||||
private disabled = false;
|
||||
private disabledReasons = new Set<string>();
|
||||
tooltip: Tooltip.TipInfo = '';
|
||||
contextMenu = true;
|
||||
|
||||
@@ -189,7 +190,14 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
/**
|
||||
* Is the current block currently in the process of being disposed?
|
||||
*/
|
||||
private disposing = false;
|
||||
protected disposing = false;
|
||||
|
||||
/**
|
||||
* Has this block been fully initialized? E.g. all fields initailized.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
initialized = false;
|
||||
|
||||
private readonly xy_: Coordinate;
|
||||
isInFlyout: boolean;
|
||||
@@ -202,7 +210,8 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
/** Name of the type of hat. */
|
||||
hat?: string;
|
||||
|
||||
rendered: boolean | null = null;
|
||||
/** Is this block a BlockSVG? */
|
||||
readonly rendered: boolean = false;
|
||||
|
||||
/**
|
||||
* String for block help, or function that returns a URL. Null for no help.
|
||||
@@ -310,8 +319,8 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
* statement with the previous statement. Otherwise, dispose of all
|
||||
* children of this block.
|
||||
*/
|
||||
dispose(healStack: boolean) {
|
||||
if (this.isDeadOrDying()) return;
|
||||
dispose(healStack = false) {
|
||||
this.disposing = true;
|
||||
|
||||
// Dispose of this change listener before unplugging.
|
||||
// Technically not necessary due to the event firing delay.
|
||||
@@ -334,15 +343,13 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
* E.g. does not fire events, unplug the block, etc.
|
||||
*/
|
||||
protected disposeInternal() {
|
||||
if (this.isDeadOrDying()) return;
|
||||
|
||||
this.disposing = true;
|
||||
if (this.onchangeWrapper_) {
|
||||
this.workspace.removeChangeListener(this.onchangeWrapper_);
|
||||
}
|
||||
|
||||
this.workspace.removeTypedBlock(this);
|
||||
this.workspace.removeBlockById(this.id);
|
||||
this.disposing = true;
|
||||
|
||||
if (typeof this.destroy === 'function') this.destroy();
|
||||
|
||||
@@ -372,13 +379,11 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
* change).
|
||||
*/
|
||||
initModel() {
|
||||
if (this.initialized) return;
|
||||
for (const input of this.inputList) {
|
||||
for (const field of input.fieldRow) {
|
||||
if (field.initModel) {
|
||||
field.initModel();
|
||||
}
|
||||
}
|
||||
input.initModel();
|
||||
}
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -559,7 +564,6 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
* connected should not coincidentally line up on screen.
|
||||
*/
|
||||
bumpNeighbours() {}
|
||||
// noop.
|
||||
|
||||
/**
|
||||
* Return the parent block or null if this block is at the top level. The
|
||||
@@ -1388,32 +1392,89 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether this block is enabled or not.
|
||||
* Get whether this block is enabled or not. A block is considered enabled
|
||||
* if there aren't any reasons why it would be disabled. A block may still
|
||||
* be disabled for other reasons even if the user attempts to manually
|
||||
* enable it, such as when the block is in an invalid location.
|
||||
*
|
||||
* @returns True if enabled.
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return !this.disabled;
|
||||
return this.disabledReasons.size === 0;
|
||||
}
|
||||
|
||||
/** @deprecated v11 - Get whether the block is manually disabled. */
|
||||
private get disabled(): boolean {
|
||||
deprecation.warn(
|
||||
'disabled',
|
||||
'v11',
|
||||
'v12',
|
||||
'the isEnabled or hasDisabledReason methods of Block',
|
||||
);
|
||||
return this.hasDisabledReason(constants.MANUALLY_DISABLED);
|
||||
}
|
||||
|
||||
/** @deprecated v11 - Set whether the block is manually disabled. */
|
||||
private set disabled(value: boolean) {
|
||||
deprecation.warn(
|
||||
'disabled',
|
||||
'v11',
|
||||
'v12',
|
||||
'the setDisabledReason method of Block',
|
||||
);
|
||||
this.setDisabledReason(value, constants.MANUALLY_DISABLED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the block is enabled or not.
|
||||
* @deprecated v11 - Set whether the block is manually enabled or disabled.
|
||||
* The user can toggle whether a block is disabled from a context menu
|
||||
* option. A block may still be disabled for other reasons even if the user
|
||||
* attempts to manually enable it, such as when the block is in an invalid
|
||||
* location. This method is deprecated and setDisabledReason should be used
|
||||
* instead.
|
||||
*
|
||||
* @param enabled True if enabled.
|
||||
*/
|
||||
setEnabled(enabled: boolean) {
|
||||
if (this.isEnabled() !== enabled) {
|
||||
const oldValue = this.disabled;
|
||||
this.disabled = !enabled;
|
||||
eventUtils.fire(
|
||||
new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
|
||||
this,
|
||||
'disabled',
|
||||
null,
|
||||
oldValue,
|
||||
!enabled,
|
||||
),
|
||||
);
|
||||
deprecation.warn(
|
||||
'setEnabled',
|
||||
'v11',
|
||||
'v12',
|
||||
'the setDisabledReason method of Block',
|
||||
);
|
||||
this.setDisabledReason(!enabled, constants.MANUALLY_DISABLED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or remove a reason why the block might be disabled. If a block has
|
||||
* any reasons to be disabled, then the block itself will be considered
|
||||
* disabled. A block could be disabled for multiple independent reasons
|
||||
* simultaneously, such as when the user manually disables it, or the block
|
||||
* is invalid.
|
||||
*
|
||||
* @param disabled If true, then the block should be considered disabled for
|
||||
* at least the provided reason, otherwise the block is no longer disabled
|
||||
* for that reason.
|
||||
* @param reason A language-neutral identifier for a reason why the block
|
||||
* could be disabled. Call this method again with the same identifier to
|
||||
* update whether the block is currently disabled for this reason.
|
||||
*/
|
||||
setDisabledReason(disabled: boolean, reason: string): void {
|
||||
if (this.disabledReasons.has(reason) !== disabled) {
|
||||
if (disabled) {
|
||||
this.disabledReasons.add(reason);
|
||||
} else {
|
||||
this.disabledReasons.delete(reason);
|
||||
}
|
||||
const blockChangeEvent = new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
|
||||
this,
|
||||
'disabled',
|
||||
/* name= */ null,
|
||||
/* oldValue= */ !disabled,
|
||||
/* newValue= */ disabled,
|
||||
) as BlockChange;
|
||||
blockChangeEvent.setDisabledReason(reason);
|
||||
eventUtils.fire(blockChangeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1426,7 +1487,7 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
getInheritedDisabled(): boolean {
|
||||
let ancestor = this.getSurroundParent();
|
||||
while (ancestor) {
|
||||
if (ancestor.disabled) {
|
||||
if (!ancestor.isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
ancestor = ancestor.getSurroundParent();
|
||||
@@ -1435,6 +1496,27 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the block is currently disabled for the provided reason.
|
||||
*
|
||||
* @param reason A language-neutral identifier for a reason why the block
|
||||
* could be disabled.
|
||||
* @returns Whether the block is disabled for the provided reason.
|
||||
*/
|
||||
hasDisabledReason(reason: string): boolean {
|
||||
return this.disabledReasons.has(reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a set of reasons why the block is currently disabled, if any. If the
|
||||
* block is enabled, this set will be empty.
|
||||
*
|
||||
* @returns The set of reasons why the block is disabled, if any.
|
||||
*/
|
||||
getDisabledReasons(): ReadonlySet<string> {
|
||||
return this.disabledReasons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the block is collapsed or not.
|
||||
*
|
||||
@@ -2208,7 +2290,7 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
* @returns Block's comment.
|
||||
*/
|
||||
getCommentText(): string | null {
|
||||
const comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | null;
|
||||
const comment = this.getIcon(IconType.COMMENT);
|
||||
return comment?.getText() ?? null;
|
||||
}
|
||||
|
||||
@@ -2218,19 +2300,36 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
* @param text The text, or null to delete.
|
||||
*/
|
||||
setCommentText(text: string | null) {
|
||||
const comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | null;
|
||||
const comment = this.getIcon(IconType.COMMENT);
|
||||
const oldText = comment?.getText() ?? null;
|
||||
if (oldText === text) return;
|
||||
if (text !== null) {
|
||||
let comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | undefined;
|
||||
let comment = this.getIcon(IconType.COMMENT);
|
||||
if (!comment) {
|
||||
comment = this.addIcon(new CommentIcon(this));
|
||||
const commentConstructor = registry.getClass(
|
||||
registry.Type.ICON,
|
||||
IconType.COMMENT.toString(),
|
||||
false,
|
||||
);
|
||||
if (!commentConstructor) {
|
||||
throw new Error(
|
||||
'No comment icon class is registered, so a comment cannot be set',
|
||||
);
|
||||
}
|
||||
const icon = new commentConstructor(this);
|
||||
if (!isCommentIcon(icon)) {
|
||||
throw new Error(
|
||||
'The class registered as a comment icon does not conform to the ' +
|
||||
'ICommentIcon interface',
|
||||
);
|
||||
}
|
||||
comment = this.addIcon(icon);
|
||||
}
|
||||
eventUtils.disable();
|
||||
comment.setText(text);
|
||||
eventUtils.enable();
|
||||
} else {
|
||||
this.removeIcon(CommentIcon.TYPE);
|
||||
this.removeIcon(IconType.COMMENT);
|
||||
}
|
||||
|
||||
eventUtils.fire(
|
||||
|
||||
@@ -1,708 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Methods for dragging a block visually.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
// Former goog.module ID: Blockly.BlockDragger
|
||||
|
||||
// Unused import preserved for side-effects. Remove if unneeded.
|
||||
import './events/events_block_drag.js';
|
||||
|
||||
import * as blockAnimation from './block_animations.js';
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import * as bumpObjects from './bump_objects.js';
|
||||
import * as common from './common.js';
|
||||
import type {BlockMove} from './events/events_block_move.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import type {Icon} from './icons/icon.js';
|
||||
import type {IBlockDragger} from './interfaces/i_block_dragger.js';
|
||||
import type {IDragTarget} from './interfaces/i_drag_target.js';
|
||||
import * as registry from './registry.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import {hasBubble} from './interfaces/i_has_bubble.js';
|
||||
import * as deprecation from './utils/deprecation.js';
|
||||
import * as layers from './layers.js';
|
||||
import {ConnectionType, IConnectionPreviewer} from './blockly.js';
|
||||
import {RenderedConnection} from './rendered_connection.js';
|
||||
import {config} from './config.js';
|
||||
import {ComponentManager} from './component_manager.js';
|
||||
import {IDeleteArea} from './interfaces/i_delete_area.js';
|
||||
import {Connection} from './connection.js';
|
||||
import {Block} from './block.js';
|
||||
import {finishQueuedRenders} from './render_management.js';
|
||||
|
||||
/** Represents a nearby valid connection. */
|
||||
interface ConnectionCandidate {
|
||||
/** A connection on the dragging stack that is compatible with neighbour. */
|
||||
local: RenderedConnection;
|
||||
|
||||
/** A nearby connection that is compatible with local. */
|
||||
neighbour: RenderedConnection;
|
||||
|
||||
/** The distance between the local connection and the neighbour connection. */
|
||||
distance: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for a block dragger. It moves blocks around the workspace when they
|
||||
* are being dragged by a mouse or touch.
|
||||
*/
|
||||
export class BlockDragger implements IBlockDragger {
|
||||
/** The top block in the stack that is being dragged. */
|
||||
protected draggingBlock_: BlockSvg;
|
||||
|
||||
protected connectionPreviewer: IConnectionPreviewer;
|
||||
|
||||
/** The workspace on which the block is being dragged. */
|
||||
protected workspace_: WorkspaceSvg;
|
||||
|
||||
/** Which drag area the mouse pointer is over, if any. */
|
||||
private dragTarget_: IDragTarget | null = null;
|
||||
|
||||
private connectionCandidate: ConnectionCandidate | null = null;
|
||||
|
||||
/** Whether the block would be deleted if dropped immediately. */
|
||||
protected wouldDeleteBlock_ = false;
|
||||
|
||||
protected startXY_: Coordinate;
|
||||
|
||||
/** The parent block at the start of the drag. */
|
||||
private startParentConn: RenderedConnection | null = null;
|
||||
|
||||
/**
|
||||
* The child block at the start of the drag. Only gets set if
|
||||
* `healStack` is true.
|
||||
*/
|
||||
private startChildConn: RenderedConnection | null = null;
|
||||
|
||||
/**
|
||||
* @deprecated To be removed in v11. Updating icons is now handled by the
|
||||
* block's `moveDuringDrag` method.
|
||||
*/
|
||||
protected dragIconData_: IconPositionData[] = [];
|
||||
|
||||
/**
|
||||
* @param block The block to drag.
|
||||
* @param workspace The workspace to drag on.
|
||||
*/
|
||||
constructor(block: BlockSvg, workspace: WorkspaceSvg) {
|
||||
this.draggingBlock_ = block;
|
||||
this.workspace_ = workspace;
|
||||
|
||||
const previewerConstructor = registry.getClassFromOptions(
|
||||
registry.Type.CONNECTION_PREVIEWER,
|
||||
this.workspace_.options,
|
||||
);
|
||||
this.connectionPreviewer = new previewerConstructor!(block);
|
||||
|
||||
/**
|
||||
* The location of the top left corner of the dragging block at the
|
||||
* beginning of the drag in workspace coordinates.
|
||||
*/
|
||||
this.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY();
|
||||
|
||||
this.dragIconData_ = initIconData(block, this.startXY_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sever all links from this object.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
dispose() {
|
||||
this.dragIconData_.length = 0;
|
||||
this.connectionPreviewer.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start dragging a block.
|
||||
*
|
||||
* @param currentDragDeltaXY How far the pointer has moved from the position
|
||||
* at mouse down, in pixel units.
|
||||
* @param healStack Whether or not to heal the stack after disconnecting.
|
||||
*/
|
||||
startDrag(currentDragDeltaXY: Coordinate, healStack: boolean) {
|
||||
if (!eventUtils.getGroup()) {
|
||||
eventUtils.setGroup(true);
|
||||
}
|
||||
this.fireDragStartEvent_();
|
||||
|
||||
// The z-order of blocks depends on their order in the SVG, so move the
|
||||
// block being dragged to the front so that it will appear atop other blocks
|
||||
// in the workspace.
|
||||
this.draggingBlock_.bringToFront(true);
|
||||
|
||||
// During a drag there may be a lot of rerenders, but not field changes.
|
||||
// Turn the cache on so we don't do spurious remeasures during the drag.
|
||||
dom.startTextWidthCache();
|
||||
this.workspace_.setResizesEnabled(false);
|
||||
blockAnimation.disconnectUiStop();
|
||||
|
||||
if (this.shouldDisconnect_(healStack)) {
|
||||
this.startParentConn =
|
||||
this.draggingBlock_.outputConnection?.targetConnection ??
|
||||
this.draggingBlock_.previousConnection?.targetConnection;
|
||||
if (healStack) {
|
||||
this.startChildConn =
|
||||
this.draggingBlock_.nextConnection?.targetConnection;
|
||||
}
|
||||
this.disconnectBlock_(healStack, currentDragDeltaXY);
|
||||
}
|
||||
this.draggingBlock_.setDragging(true);
|
||||
this.workspace_.getLayerManager()?.moveToDragLayer(this.draggingBlock_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we should disconnect the block when a drag is started.
|
||||
*
|
||||
* @param healStack Whether or not to heal the stack after disconnecting.
|
||||
* @returns True to disconnect the block, false otherwise.
|
||||
*/
|
||||
protected shouldDisconnect_(healStack: boolean): boolean {
|
||||
return !!(
|
||||
this.draggingBlock_.getParent() ||
|
||||
(healStack &&
|
||||
this.draggingBlock_.nextConnection &&
|
||||
this.draggingBlock_.nextConnection.targetBlock())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the block and moves it to a new location.
|
||||
*
|
||||
* @param healStack Whether or not to heal the stack after disconnecting.
|
||||
* @param currentDragDeltaXY How far the pointer has moved from the position
|
||||
* at mouse down, in pixel units.
|
||||
*/
|
||||
protected disconnectBlock_(
|
||||
healStack: boolean,
|
||||
currentDragDeltaXY: Coordinate,
|
||||
) {
|
||||
this.draggingBlock_.unplug(healStack);
|
||||
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
|
||||
const newLoc = Coordinate.sum(this.startXY_, delta);
|
||||
|
||||
this.draggingBlock_.translate(newLoc.x, newLoc.y);
|
||||
blockAnimation.disconnectUiEffect(this.draggingBlock_);
|
||||
}
|
||||
|
||||
/** Fire a UI event at the start of a block drag. */
|
||||
protected fireDragStartEvent_() {
|
||||
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
|
||||
this.draggingBlock_,
|
||||
true,
|
||||
this.draggingBlock_.getDescendants(false),
|
||||
);
|
||||
eventUtils.fire(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a step of block dragging, based on the given event. Update the
|
||||
* display accordingly.
|
||||
*
|
||||
* @param e The most recent move event.
|
||||
* @param delta How far the pointer has moved from the position
|
||||
* at the start of the drag, in pixel units.
|
||||
*/
|
||||
drag(e: PointerEvent, delta: Coordinate) {
|
||||
const block = this.draggingBlock_;
|
||||
this.moveBlock(block, delta);
|
||||
this.updateDragTargets(e, block);
|
||||
this.wouldDeleteBlock_ = this.wouldDeleteBlock(e, block, delta);
|
||||
this.updateCursorDuringBlockDrag_();
|
||||
this.updateConnectionPreview(block, delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param draggingBlock The block being dragged.
|
||||
* @param dragDelta How far the pointer has moved from the position
|
||||
* at the start of the drag, in pixel units.
|
||||
*/
|
||||
private moveBlock(draggingBlock: BlockSvg, dragDelta: Coordinate) {
|
||||
const delta = this.pixelsToWorkspaceUnits_(dragDelta);
|
||||
const newLoc = Coordinate.sum(this.startXY_, delta);
|
||||
draggingBlock.moveDuringDrag(newLoc);
|
||||
}
|
||||
|
||||
private updateDragTargets(e: PointerEvent, draggingBlock: BlockSvg) {
|
||||
const newDragTarget = this.workspace_.getDragTarget(e);
|
||||
if (this.dragTarget_ !== newDragTarget) {
|
||||
this.dragTarget_?.onDragExit(draggingBlock);
|
||||
newDragTarget?.onDragEnter(draggingBlock);
|
||||
}
|
||||
newDragTarget?.onDragOver(draggingBlock);
|
||||
this.dragTarget_ = newDragTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we would delete the block if it was dropped at this time,
|
||||
* false otherwise.
|
||||
*
|
||||
* @param e The most recent move event.
|
||||
* @param draggingBlock The block being dragged.
|
||||
* @param delta How far the pointer has moved from the position
|
||||
* at the start of the drag, in pixel units.
|
||||
*/
|
||||
private wouldDeleteBlock(
|
||||
e: PointerEvent,
|
||||
draggingBlock: BlockSvg,
|
||||
delta: Coordinate,
|
||||
): boolean {
|
||||
const dragTarget = this.workspace_.getDragTarget(e);
|
||||
if (!dragTarget) return false;
|
||||
|
||||
const componentManager = this.workspace_.getComponentManager();
|
||||
const isDeleteArea = componentManager.hasCapability(
|
||||
dragTarget.id,
|
||||
ComponentManager.Capability.DELETE_AREA,
|
||||
);
|
||||
if (!isDeleteArea) return false;
|
||||
|
||||
return (dragTarget as IDeleteArea).wouldDelete(
|
||||
draggingBlock,
|
||||
!!this.getConnectionCandidate(draggingBlock, delta),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param draggingBlock The block being dragged.
|
||||
* @param dragDelta How far the pointer has moved from the position
|
||||
* at the start of the drag, in pixel units.
|
||||
*/
|
||||
private updateConnectionPreview(
|
||||
draggingBlock: BlockSvg,
|
||||
dragDelta: Coordinate,
|
||||
) {
|
||||
const delta = this.pixelsToWorkspaceUnits_(dragDelta);
|
||||
const currCandidate = this.connectionCandidate;
|
||||
const newCandidate = this.getConnectionCandidate(draggingBlock, delta);
|
||||
if (!newCandidate) {
|
||||
this.connectionPreviewer.hidePreview();
|
||||
this.connectionCandidate = null;
|
||||
return;
|
||||
}
|
||||
const candidate =
|
||||
currCandidate &&
|
||||
this.currCandidateIsBetter(currCandidate, delta, newCandidate)
|
||||
? currCandidate
|
||||
: newCandidate;
|
||||
this.connectionCandidate = candidate;
|
||||
const {local, neighbour} = candidate;
|
||||
if (
|
||||
(local.type === ConnectionType.OUTPUT_VALUE ||
|
||||
local.type === ConnectionType.PREVIOUS_STATEMENT) &&
|
||||
neighbour.isConnected() &&
|
||||
!neighbour.targetBlock()!.isInsertionMarker() &&
|
||||
!this.orphanCanConnectAtEnd(
|
||||
draggingBlock,
|
||||
neighbour.targetBlock()!,
|
||||
local.type,
|
||||
)
|
||||
) {
|
||||
this.connectionPreviewer.previewReplacement(
|
||||
local,
|
||||
neighbour,
|
||||
neighbour.targetBlock()!,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.connectionPreviewer.previewConnection(local, neighbour);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given orphan block can connect at the end of the
|
||||
* top block's stack or row, false otherwise.
|
||||
*/
|
||||
private orphanCanConnectAtEnd(
|
||||
topBlock: BlockSvg,
|
||||
orphanBlock: BlockSvg,
|
||||
localType: number,
|
||||
): boolean {
|
||||
const orphanConnection =
|
||||
localType === ConnectionType.OUTPUT_VALUE
|
||||
? orphanBlock.outputConnection
|
||||
: orphanBlock.previousConnection;
|
||||
return !!Connection.getConnectionForOrphanedConnection(
|
||||
topBlock as Block,
|
||||
orphanConnection as Connection,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current candidate is better than the new candidate.
|
||||
*
|
||||
* We slightly prefer the current candidate even if it is farther away.
|
||||
*/
|
||||
private currCandidateIsBetter(
|
||||
currCandiate: ConnectionCandidate,
|
||||
delta: Coordinate,
|
||||
newCandidate: ConnectionCandidate,
|
||||
): boolean {
|
||||
const {local: currLocal, neighbour: currNeighbour} = currCandiate;
|
||||
const localPos = new Coordinate(currLocal.x, currLocal.y);
|
||||
const neighbourPos = new Coordinate(currNeighbour.x, currNeighbour.y);
|
||||
const distance = Coordinate.distance(
|
||||
Coordinate.sum(localPos, delta),
|
||||
neighbourPos,
|
||||
);
|
||||
return (
|
||||
newCandidate.distance > distance - config.currentConnectionPreference
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the closest valid candidate connection, if one can be found.
|
||||
*
|
||||
* Valid neighbour connections are within the configured start radius, with a
|
||||
* compatible type (input, output, etc) and connection check.
|
||||
*/
|
||||
private getConnectionCandidate(
|
||||
draggingBlock: BlockSvg,
|
||||
delta: Coordinate,
|
||||
): ConnectionCandidate | null {
|
||||
const localConns = this.getLocalConnections(draggingBlock);
|
||||
let radius = this.connectionCandidate
|
||||
? config.connectingSnapRadius
|
||||
: config.snapRadius;
|
||||
let candidate = null;
|
||||
|
||||
for (const conn of localConns) {
|
||||
const {connection: neighbour, radius: rad} = conn.closest(radius, delta);
|
||||
if (neighbour) {
|
||||
candidate = {
|
||||
local: conn,
|
||||
neighbour: neighbour,
|
||||
distance: rad,
|
||||
};
|
||||
radius = rad;
|
||||
}
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of the connections we might connect to blocks on the workspace.
|
||||
*
|
||||
* Includes any connections on the dragging block, and any last next
|
||||
* connection on the stack (if one exists).
|
||||
*/
|
||||
private getLocalConnections(draggingBlock: BlockSvg): RenderedConnection[] {
|
||||
const available = draggingBlock.getConnections_(false);
|
||||
const lastOnStack = draggingBlock.lastConnectionInStack(true);
|
||||
if (lastOnStack && lastOnStack !== draggingBlock.nextConnection) {
|
||||
available.push(lastOnStack);
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish a block drag and put the block back on the workspace.
|
||||
*
|
||||
* @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: PointerEvent, currentDragDeltaXY: Coordinate) {
|
||||
// Make sure internal state is fresh.
|
||||
this.drag(e, currentDragDeltaXY);
|
||||
this.fireDragEndEvent_();
|
||||
|
||||
dom.stopTextWidthCache();
|
||||
|
||||
blockAnimation.disconnectUiStop();
|
||||
this.connectionPreviewer.hidePreview();
|
||||
|
||||
const preventMove =
|
||||
!!this.dragTarget_ &&
|
||||
this.dragTarget_.shouldPreventMove(this.draggingBlock_);
|
||||
let delta: Coordinate | null = null;
|
||||
if (!preventMove) {
|
||||
const newValues = this.getNewLocationAfterDrag_(currentDragDeltaXY);
|
||||
delta = newValues.delta;
|
||||
}
|
||||
|
||||
if (this.dragTarget_) {
|
||||
this.dragTarget_.onDrop(this.draggingBlock_);
|
||||
}
|
||||
|
||||
const deleted = this.maybeDeleteBlock_();
|
||||
if (!deleted) {
|
||||
// These are expensive and don't need to be done if we're deleting.
|
||||
this.workspace_
|
||||
.getLayerManager()
|
||||
?.moveOffDragLayer(this.draggingBlock_, layers.BLOCK);
|
||||
this.draggingBlock_.setDragging(false);
|
||||
if (preventMove) {
|
||||
this.moveToOriginalPosition();
|
||||
} else if (delta) {
|
||||
this.updateBlockAfterMove_();
|
||||
}
|
||||
}
|
||||
// Must dispose after `updateBlockAfterMove_` is called to not break the
|
||||
// dynamic connections plugin.
|
||||
this.connectionPreviewer.dispose();
|
||||
this.workspace_.setResizesEnabled(true);
|
||||
|
||||
eventUtils.setGroup(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the dragged block back to its original position before the start of
|
||||
* the drag. Reconnects any parent and child blocks.
|
||||
*/
|
||||
private moveToOriginalPosition() {
|
||||
this.startChildConn?.connect(this.draggingBlock_.nextConnection);
|
||||
if (this.startParentConn) {
|
||||
switch (this.startParentConn.type) {
|
||||
case ConnectionType.INPUT_VALUE:
|
||||
this.startParentConn.connect(this.draggingBlock_.outputConnection);
|
||||
break;
|
||||
case ConnectionType.NEXT_STATEMENT:
|
||||
this.startParentConn.connect(this.draggingBlock_.previousConnection);
|
||||
}
|
||||
} else {
|
||||
this.draggingBlock_.moveTo(this.startXY_, ['drag']);
|
||||
// Blocks dragged directly from a flyout may need to be bumped into
|
||||
// bounds.
|
||||
bumpObjects.bumpIntoBounds(
|
||||
this.draggingBlock_.workspace,
|
||||
this.workspace_.getMetricsManager().getScrollMetrics(true),
|
||||
this.draggingBlock_,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the drag delta and new location values after a block is dragged.
|
||||
*
|
||||
* @param currentDragDeltaXY How far the pointer has moved from the start of
|
||||
* the drag, in pixel units.
|
||||
* @returns New location after drag. delta is in workspace units. newLocation
|
||||
* is the new coordinate where the block should end up.
|
||||
*/
|
||||
protected getNewLocationAfterDrag_(currentDragDeltaXY: Coordinate): {
|
||||
delta: Coordinate;
|
||||
newLocation: Coordinate;
|
||||
} {
|
||||
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
|
||||
const newLocation = Coordinate.sum(this.startXY_, delta);
|
||||
return {
|
||||
delta,
|
||||
newLocation,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* May delete the dragging block, if allowed. If `this.wouldDeleteBlock_` is
|
||||
* not true, the block will not be deleted. This should be called at the end
|
||||
* of a block drag.
|
||||
*
|
||||
* @returns True if the block was deleted.
|
||||
*/
|
||||
protected maybeDeleteBlock_(): boolean {
|
||||
if (this.wouldDeleteBlock_) {
|
||||
// Fire a move event, so we know where to go back to for an undo.
|
||||
this.fireMoveEvent_();
|
||||
this.draggingBlock_.dispose(false, true);
|
||||
common.draggingConnections.length = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the necessary information to place a block at a certain location.
|
||||
*/
|
||||
protected updateBlockAfterMove_() {
|
||||
this.fireMoveEvent_();
|
||||
if (this.connectionCandidate) {
|
||||
// Applying connections also rerenders the relevant blocks.
|
||||
this.applyConnections(this.connectionCandidate);
|
||||
} else {
|
||||
this.draggingBlock_.queueRender();
|
||||
}
|
||||
this.draggingBlock_.scheduleSnapAndBump();
|
||||
}
|
||||
|
||||
private applyConnections(candidate: ConnectionCandidate) {
|
||||
const {local, neighbour} = candidate;
|
||||
local.connect(neighbour);
|
||||
// TODO: We can remove this `rendered` check when we reconcile with v11.
|
||||
if (this.draggingBlock_.rendered) {
|
||||
const inferiorConnection = local.isSuperior() ? neighbour : local;
|
||||
const rootBlock = this.draggingBlock_.getRootBlock();
|
||||
|
||||
finishQueuedRenders().then(() => {
|
||||
blockAnimation.connectionUiEffect(inferiorConnection.getSourceBlock());
|
||||
// bringToFront is incredibly expensive. Delay until the next frame.
|
||||
setTimeout(() => {
|
||||
rootBlock.bringToFront();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Fire a UI event at the end of a block drag. */
|
||||
protected fireDragEndEvent_() {
|
||||
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
|
||||
this.draggingBlock_,
|
||||
false,
|
||||
this.draggingBlock_.getDescendants(false),
|
||||
);
|
||||
eventUtils.fire(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or removes the style of the cursor for the toolbox.
|
||||
* This is what changes the cursor to display an x when a deletable block is
|
||||
* held over the toolbox.
|
||||
*
|
||||
* @param isEnd True if we are at the end of a drag, false otherwise.
|
||||
*/
|
||||
protected updateToolboxStyle_(isEnd: boolean) {
|
||||
const toolbox = this.workspace_.getToolbox();
|
||||
|
||||
if (toolbox) {
|
||||
const style = this.draggingBlock_.isDeletable()
|
||||
? 'blocklyToolboxDelete'
|
||||
: 'blocklyToolboxGrab';
|
||||
|
||||
// AnyDuringMigration because: Property 'removeStyle' does not exist on
|
||||
// type 'IToolbox'.
|
||||
if (
|
||||
isEnd &&
|
||||
typeof (toolbox as AnyDuringMigration).removeStyle === 'function'
|
||||
) {
|
||||
// AnyDuringMigration because: Property 'removeStyle' does not exist on
|
||||
// type 'IToolbox'.
|
||||
(toolbox as AnyDuringMigration).removeStyle(style);
|
||||
// AnyDuringMigration because: Property 'addStyle' does not exist on
|
||||
// type 'IToolbox'.
|
||||
} else if (
|
||||
!isEnd &&
|
||||
typeof (toolbox as AnyDuringMigration).addStyle === 'function'
|
||||
) {
|
||||
// AnyDuringMigration because: Property 'addStyle' does not exist on
|
||||
// type 'IToolbox'.
|
||||
(toolbox as AnyDuringMigration).addStyle(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Fire a move event at the end of a block drag. */
|
||||
protected fireMoveEvent_() {
|
||||
if (this.draggingBlock_.isDeadOrDying()) return;
|
||||
const event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(
|
||||
this.draggingBlock_,
|
||||
) as BlockMove;
|
||||
event.setReason(['drag']);
|
||||
event.oldCoordinate = this.startXY_;
|
||||
event.recordNew();
|
||||
eventUtils.fire(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cursor (and possibly the trash can lid) to reflect whether the
|
||||
* dragging block would be deleted if released immediately.
|
||||
*/
|
||||
protected updateCursorDuringBlockDrag_() {
|
||||
this.draggingBlock_.setDeleteStyle(this.wouldDeleteBlock_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a coordinate object from pixels to workspace units, including a
|
||||
* correction for mutator workspaces.
|
||||
* This function does not consider differing origins. It simply scales the
|
||||
* input's x and y values.
|
||||
*
|
||||
* @param pixelCoord A coordinate with x and y values in CSS pixel units.
|
||||
* @returns The input coordinate divided by the workspace scale.
|
||||
*/
|
||||
protected pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate {
|
||||
const result = new Coordinate(
|
||||
pixelCoord.x / this.workspace_.scale,
|
||||
pixelCoord.y / this.workspace_.scale,
|
||||
);
|
||||
if (this.workspace_.isMutator) {
|
||||
// If we're in a mutator, its scale is always 1, purely because of some
|
||||
// oddities in our rendering optimizations. The actual scale is the same
|
||||
// as the scale on the parent workspace. Fix that for dragging.
|
||||
const mainScale = this.workspace_.options.parentWorkspace!.scale;
|
||||
result.scale(1 / mainScale);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move all of the icons connected to this drag.
|
||||
*
|
||||
* @deprecated To be removed in v11. This is now handled by the block's
|
||||
* `moveDuringDrag` method.
|
||||
*/
|
||||
protected dragIcons_() {
|
||||
deprecation.warn('Blockly.BlockDragger.prototype.dragIcons_', 'v10', 'v11');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of the insertion markers that currently exist. Drags have 0, 1,
|
||||
* or 2 insertion markers.
|
||||
*
|
||||
* @returns A possibly empty list of insertion marker blocks.
|
||||
*/
|
||||
getInsertionMarkers(): BlockSvg[] {
|
||||
return this.workspace_
|
||||
.getAllBlocks()
|
||||
.filter((block) => block.isInsertionMarker());
|
||||
}
|
||||
}
|
||||
|
||||
/** Data about the position of a given icon. */
|
||||
export interface IconPositionData {
|
||||
location: Coordinate;
|
||||
icon: Icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a list of all of the icons (comment, warning, and mutator) that are
|
||||
* on this block and its descendants. Moving an icon moves the bubble that
|
||||
* extends from it if that bubble is open.
|
||||
*
|
||||
* @param block The root block that is being dragged.
|
||||
* @param blockOrigin The top left of the given block in workspace coordinates.
|
||||
* @returns The list of all icons and their locations.
|
||||
*/
|
||||
function initIconData(
|
||||
block: BlockSvg,
|
||||
blockOrigin: Coordinate,
|
||||
): IconPositionData[] {
|
||||
// Build a list of icons that need to be moved and where they started.
|
||||
const dragIconData = [];
|
||||
|
||||
for (const icon of block.getIcons()) {
|
||||
// Only bother to track icons whose bubble is visible.
|
||||
if (hasBubble(icon) && !icon.bubbleIsVisible()) continue;
|
||||
|
||||
dragIconData.push({location: blockOrigin, icon: icon});
|
||||
icon.onLocationChange(blockOrigin);
|
||||
}
|
||||
|
||||
for (const child of block.getChildren(false)) {
|
||||
dragIconData.push(
|
||||
...initIconData(child, Coordinate.sum(blockOrigin, child.relativeCoords)),
|
||||
);
|
||||
}
|
||||
// AnyDuringMigration because: Type '{ location: Coordinate | null; icon:
|
||||
// Icon; }[]' is not assignable to type 'IconPositionData[]'.
|
||||
return dragIconData as AnyDuringMigration;
|
||||
}
|
||||
|
||||
registry.register(registry.Type.BLOCK_DRAGGER, registry.DEFAULT, BlockDragger);
|
||||
@@ -17,7 +17,6 @@ import './events/events_selected.js';
|
||||
import {Block} from './block.js';
|
||||
import * as blockAnimations from './block_animations.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import {CommentIcon} from './icons/comment_icon.js';
|
||||
import * as common from './common.js';
|
||||
import {config} from './config.js';
|
||||
import type {Connection} from './connection.js';
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
LegacyContextMenuOption,
|
||||
} from './contextmenu_registry.js';
|
||||
import type {BlockMove} from './events/events_block_move.js';
|
||||
import * as deprecation from './utils/deprecation.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import type {Field} from './field.js';
|
||||
import {FieldLabel} from './field_label.js';
|
||||
@@ -37,7 +37,7 @@ import type {Input} from './inputs/input.js';
|
||||
import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
|
||||
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||
import type {ICopyable} from './interfaces/i_copyable.js';
|
||||
import type {IDraggable} from './interfaces/i_draggable.js';
|
||||
import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js';
|
||||
import {IIcon} from './interfaces/i_icon.js';
|
||||
import * as internalConstants from './internal_constants.js';
|
||||
import {ASTNode} from './keyboard_nav/ast_node.js';
|
||||
@@ -59,9 +59,11 @@ import {WarningIcon} from './icons/warning_icon.js';
|
||||
import type {Workspace} from './workspace.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import * as renderManagement from './render_management.js';
|
||||
import * as deprecation from './utils/deprecation.js';
|
||||
import {IconType} from './icons/icon_types.js';
|
||||
import {BlockCopyData, BlockPaster} from './clipboard/block_paster.js';
|
||||
import {BlockDragStrategy} from './dragging/block_drag_strategy.js';
|
||||
import {IDeletable} from './blockly.js';
|
||||
import {FlyoutItemInfo} from './utils/toolbox.js';
|
||||
|
||||
/**
|
||||
* Class for a block's SVG representation.
|
||||
@@ -73,7 +75,8 @@ export class BlockSvg
|
||||
IASTNodeLocationSvg,
|
||||
IBoundedElement,
|
||||
ICopyable<BlockCopyData>,
|
||||
IDraggable
|
||||
IDraggable,
|
||||
IDeletable
|
||||
{
|
||||
/**
|
||||
* Constant for identifying rows that are to be rendered inline.
|
||||
@@ -115,18 +118,14 @@ export class BlockSvg
|
||||
/** Block's mutator icon (if any). */
|
||||
mutator: MutatorIcon | null = null;
|
||||
|
||||
/**
|
||||
* Block's warning icon (if any).
|
||||
*
|
||||
* @deprecated Use `setWarningText` to modify warnings on this block.
|
||||
*/
|
||||
warning: WarningIcon | null = null;
|
||||
|
||||
private svgGroup_: SVGGElement;
|
||||
style: BlockStyle;
|
||||
/** @internal */
|
||||
pathObject: IPathObject;
|
||||
override rendered = false;
|
||||
|
||||
/** Is this block a BlockSVG? */
|
||||
override readonly rendered = true;
|
||||
|
||||
private visuallyDisabled = false;
|
||||
|
||||
/**
|
||||
@@ -148,12 +147,6 @@ export class BlockSvg
|
||||
|
||||
private translation = '';
|
||||
|
||||
/**
|
||||
* The ID of the setTimeout callback for bumping neighbours, or 0 if no bump
|
||||
* is currently scheduled.
|
||||
*/
|
||||
private bumpNeighboursPid = 0;
|
||||
|
||||
/** Whether this block is currently being dragged. */
|
||||
private dragging = false;
|
||||
|
||||
@@ -166,6 +159,8 @@ export class BlockSvg
|
||||
*/
|
||||
relativeCoords = new Coordinate(0, 0);
|
||||
|
||||
private dragStrategy: IDragStrategy = new BlockDragStrategy(this);
|
||||
|
||||
/**
|
||||
* @param workspace The block's workspace.
|
||||
* @param prototypeName Name of the language object containing type-specific
|
||||
@@ -175,6 +170,9 @@ export class BlockSvg
|
||||
*/
|
||||
constructor(workspace: WorkspaceSvg, prototypeName: string, opt_id?: string) {
|
||||
super(workspace, prototypeName, opt_id);
|
||||
if (!workspace.rendered) {
|
||||
throw TypeError('Cannot create a rendered block in a headless workspace');
|
||||
}
|
||||
this.workspace = workspace;
|
||||
this.svgGroup_ = dom.createSvgElement(Svg.G, {});
|
||||
|
||||
@@ -201,10 +199,8 @@ export class BlockSvg
|
||||
* May be called more than once.
|
||||
*/
|
||||
initSvg() {
|
||||
if (!this.workspace.rendered) {
|
||||
throw TypeError('Workspace is headless.');
|
||||
}
|
||||
for (let i = 0, input; (input = this.inputList[i]); i++) {
|
||||
if (this.initialized) return;
|
||||
for (const input of this.inputList) {
|
||||
input.init();
|
||||
}
|
||||
for (const icon of this.getIcons()) {
|
||||
@@ -214,7 +210,7 @@ export class BlockSvg
|
||||
this.applyColour();
|
||||
this.pathObject.updateMovable(this.isMovable());
|
||||
const svg = this.getSvgRoot();
|
||||
if (!this.workspace.options.readOnly && !this.eventsInit_ && svg) {
|
||||
if (!this.workspace.options.readOnly && svg) {
|
||||
browserEvents.conditionalBind(
|
||||
svg,
|
||||
'pointerdown',
|
||||
@@ -222,11 +218,11 @@ export class BlockSvg
|
||||
this.onMouseDown_,
|
||||
);
|
||||
}
|
||||
this.eventsInit_ = true;
|
||||
|
||||
if (!svg.parentNode) {
|
||||
this.workspace.getCanvas().appendChild(svg);
|
||||
}
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,56 +243,21 @@ export class BlockSvg
|
||||
return this.style.colourTertiary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects this block. Highlights the block visually and fires a select event
|
||||
* if the block is not already selected.
|
||||
*/
|
||||
/** Selects this block. Highlights the block visually. */
|
||||
select() {
|
||||
if (this.isShadow() && this.getParent()) {
|
||||
// Shadow blocks should not be selected.
|
||||
this.getParent()!.select();
|
||||
if (this.isShadow()) {
|
||||
this.getParent()?.select();
|
||||
return;
|
||||
}
|
||||
if (common.getSelected() === this) {
|
||||
return;
|
||||
}
|
||||
let oldId = null;
|
||||
if (common.getSelected()) {
|
||||
oldId = common.getSelected()!.id;
|
||||
// Unselect any previously selected block.
|
||||
eventUtils.disable();
|
||||
try {
|
||||
common.getSelected()!.unselect();
|
||||
} finally {
|
||||
eventUtils.enable();
|
||||
}
|
||||
}
|
||||
const event = new (eventUtils.get(eventUtils.SELECTED))(
|
||||
oldId,
|
||||
this.id,
|
||||
this.workspace.id,
|
||||
);
|
||||
eventUtils.fire(event);
|
||||
common.setSelected(this);
|
||||
this.addSelect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselects this block. Unhighlights the block and fires a select (false)
|
||||
* event if the block is currently selected.
|
||||
*/
|
||||
/** Unselects this block. Unhighlights the blockv visually. */
|
||||
unselect() {
|
||||
if (common.getSelected() !== this) {
|
||||
if (this.isShadow()) {
|
||||
this.getParent()?.unselect();
|
||||
return;
|
||||
}
|
||||
const event = new (eventUtils.get(eventUtils.SELECTED))(
|
||||
this.id,
|
||||
null,
|
||||
this.workspace.id,
|
||||
);
|
||||
event.workspaceId = this.workspace.id;
|
||||
eventUtils.fire(event);
|
||||
common.setSelected(null);
|
||||
this.removeSelect();
|
||||
}
|
||||
|
||||
@@ -455,34 +416,15 @@ export class BlockSvg
|
||||
|
||||
/** Snap this block to the nearest grid point. */
|
||||
snapToGrid() {
|
||||
if (this.isDeadOrDying()) {
|
||||
return; // Deleted block.
|
||||
}
|
||||
if (this.workspace.isDragging()) {
|
||||
return; // Don't bump blocks during a drag.
|
||||
}
|
||||
|
||||
if (this.getParent()) {
|
||||
return; // Only snap top-level blocks.
|
||||
}
|
||||
if (this.isInFlyout) {
|
||||
return; // Don't move blocks around in a flyout.
|
||||
}
|
||||
if (this.isDeadOrDying()) return;
|
||||
if (this.getParent()) return;
|
||||
if (this.isInFlyout) return;
|
||||
const grid = this.workspace.getGrid();
|
||||
if (!grid || !grid.shouldSnap()) {
|
||||
return; // Config says no snapping.
|
||||
}
|
||||
const spacing = grid.getSpacing();
|
||||
const half = spacing / 2;
|
||||
const xy = this.getRelativeToSurfaceXY();
|
||||
const dx = Math.round(
|
||||
Math.round((xy.x - half) / spacing) * spacing + half - xy.x,
|
||||
);
|
||||
const dy = Math.round(
|
||||
Math.round((xy.y - half) / spacing) * spacing + half - xy.y,
|
||||
);
|
||||
if (dx || dy) {
|
||||
this.moveBy(dx, dy, ['snap']);
|
||||
if (!grid?.shouldSnap()) return;
|
||||
const currentXY = this.getRelativeToSurfaceXY();
|
||||
const alignedXY = grid.alignXY(currentXY);
|
||||
if (alignedXY !== currentXY) {
|
||||
this.moveTo(alignedXY, ['snap']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,7 +595,7 @@ export class BlockSvg
|
||||
* @param e Mouse event.
|
||||
* @internal
|
||||
*/
|
||||
showContextMenu(e: Event) {
|
||||
showContextMenu(e: PointerEvent) {
|
||||
const menuOptions = this.generateContextMenu();
|
||||
|
||||
if (menuOptions && menuOptions.length) {
|
||||
@@ -670,12 +612,6 @@ export class BlockSvg
|
||||
* @internal
|
||||
*/
|
||||
updateComponentLocations(blockOrigin: Coordinate) {
|
||||
if (!this.rendered) {
|
||||
// Rendering is required to lay out the blocks.
|
||||
// This is probably an invisible block attached to a collapsed block.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.dragging) this.updateConnectionLocations(blockOrigin);
|
||||
this.updateIconLocations(blockOrigin);
|
||||
this.updateFieldLocations(blockOrigin);
|
||||
@@ -804,12 +740,12 @@ export class BlockSvg
|
||||
* @param animate If true, show a disposal animation and sound.
|
||||
*/
|
||||
override dispose(healStack?: boolean, animate?: boolean) {
|
||||
if (this.isDeadOrDying()) return;
|
||||
this.disposing = true;
|
||||
|
||||
Tooltip.dispose();
|
||||
ContextMenu.hide();
|
||||
|
||||
if (animate && this.rendered) {
|
||||
if (animate) {
|
||||
this.unplug(healStack);
|
||||
blockAnimations.disposeUiEffect(this);
|
||||
}
|
||||
@@ -823,11 +759,9 @@ export class BlockSvg
|
||||
* E.g. does trigger UI effects, remove nodes, etc.
|
||||
*/
|
||||
override disposeInternal() {
|
||||
if (this.isDeadOrDying()) return;
|
||||
this.disposing = true;
|
||||
super.disposeInternal();
|
||||
|
||||
this.rendered = false;
|
||||
|
||||
if (common.getSelected() === this) {
|
||||
this.unselect();
|
||||
this.workspace.cancelCurrentGesture();
|
||||
@@ -922,18 +856,6 @@ export class BlockSvg
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comment icon attached to this block, or null if the block has no
|
||||
* comment.
|
||||
*
|
||||
* @returns The comment icon attached to this block, or null.
|
||||
* @deprecated Use getIcon. To be remove in v11.
|
||||
*/
|
||||
getCommentIcon(): CommentIcon | null {
|
||||
deprecation.warn('getCommentIcon', 'v10', 'v11', 'getIcon');
|
||||
return (this.getIcon(CommentIcon.TYPE) ?? null) as CommentIcon | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this block's warning text.
|
||||
*
|
||||
@@ -1021,17 +943,12 @@ export class BlockSvg
|
||||
override addIcon<T extends IIcon>(icon: T): T {
|
||||
super.addIcon(icon);
|
||||
|
||||
if (icon instanceof WarningIcon) this.warning = icon;
|
||||
if (icon instanceof MutatorIcon) this.mutator = icon;
|
||||
|
||||
if (this.rendered) {
|
||||
icon.initView(this.createIconPointerDownListener(icon));
|
||||
icon.applyColour();
|
||||
icon.updateEditable();
|
||||
this.queueRender();
|
||||
renderManagement.triggerQueuedRenders();
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
icon.initView(this.createIconPointerDownListener(icon));
|
||||
icon.applyColour();
|
||||
icon.updateEditable();
|
||||
this.queueRender();
|
||||
|
||||
return icon;
|
||||
}
|
||||
@@ -1053,28 +970,56 @@ export class BlockSvg
|
||||
override removeIcon(type: IconType<IIcon>): boolean {
|
||||
const removed = super.removeIcon(type);
|
||||
|
||||
if (type.equals(WarningIcon.TYPE)) this.warning = null;
|
||||
if (type.equals(MutatorIcon.TYPE)) this.mutator = null;
|
||||
|
||||
if (this.rendered) {
|
||||
this.queueRender();
|
||||
renderManagement.triggerQueuedRenders();
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
this.queueRender();
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the block is enabled or not.
|
||||
* @deprecated v11 - Set whether the block is manually enabled or disabled.
|
||||
* The user can toggle whether a block is disabled from a context menu
|
||||
* option. A block may still be disabled for other reasons even if the user
|
||||
* attempts to manually enable it, such as when the block is in an invalid
|
||||
* location. This method is deprecated and setDisabledReason should be used
|
||||
* instead.
|
||||
*
|
||||
* @param enabled True if enabled.
|
||||
*/
|
||||
override setEnabled(enabled: boolean) {
|
||||
if (this.isEnabled() !== enabled) {
|
||||
super.setEnabled(enabled);
|
||||
if (this.rendered && !this.getInheritedDisabled()) {
|
||||
this.updateDisabled();
|
||||
}
|
||||
deprecation.warn(
|
||||
'setEnabled',
|
||||
'v11',
|
||||
'v12',
|
||||
'the setDisabledReason method of BlockSvg',
|
||||
);
|
||||
const wasEnabled = this.isEnabled();
|
||||
super.setEnabled(enabled);
|
||||
if (this.isEnabled() !== wasEnabled && !this.getInheritedDisabled()) {
|
||||
this.updateDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or remove a reason why the block might be disabled. If a block has
|
||||
* any reasons to be disabled, then the block itself will be considered
|
||||
* disabled. A block could be disabled for multiple independent reasons
|
||||
* simultaneously, such as when the user manually disables it, or the block
|
||||
* is invalid.
|
||||
*
|
||||
* @param disabled If true, then the block should be considered disabled for
|
||||
* at least the provided reason, otherwise the block is no longer disabled
|
||||
* for that reason.
|
||||
* @param reason A language-neutral identifier for a reason why the block
|
||||
* could be disabled. Call this method again with the same identifier to
|
||||
* update whether the block is currently disabled for this reason.
|
||||
*/
|
||||
override setDisabledReason(disabled: boolean, reason: string): void {
|
||||
const wasEnabled = this.isEnabled();
|
||||
super.setDisabledReason(disabled, reason);
|
||||
if (this.isEnabled() !== wasEnabled && !this.getInheritedDisabled()) {
|
||||
this.updateDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1085,9 +1030,6 @@ export class BlockSvg
|
||||
* @param highlighted True if highlighted.
|
||||
*/
|
||||
setHighlighted(highlighted: boolean) {
|
||||
if (!this.rendered) {
|
||||
return;
|
||||
}
|
||||
this.pathObject.updateHighlighted(highlighted);
|
||||
}
|
||||
|
||||
@@ -1219,11 +1161,7 @@ export class BlockSvg
|
||||
opt_check?: string | string[] | null,
|
||||
) {
|
||||
super.setPreviousStatement(newBoolean, opt_check);
|
||||
|
||||
if (this.rendered) {
|
||||
this.queueRender();
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
this.queueRender();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1238,11 +1176,7 @@ export class BlockSvg
|
||||
opt_check?: string | string[] | null,
|
||||
) {
|
||||
super.setNextStatement(newBoolean, opt_check);
|
||||
|
||||
if (this.rendered) {
|
||||
this.queueRender();
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
this.queueRender();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1257,11 +1191,7 @@ export class BlockSvg
|
||||
opt_check?: string | string[] | null,
|
||||
) {
|
||||
super.setOutput(newBoolean, opt_check);
|
||||
|
||||
if (this.rendered) {
|
||||
this.queueRender();
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
this.queueRender();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1271,11 +1201,7 @@ export class BlockSvg
|
||||
*/
|
||||
override setInputsInline(newBoolean: boolean) {
|
||||
super.setInputsInline(newBoolean);
|
||||
|
||||
if (this.rendered) {
|
||||
this.queueRender();
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
this.queueRender();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1289,13 +1215,7 @@ export class BlockSvg
|
||||
*/
|
||||
override removeInput(name: string, opt_quiet?: boolean): boolean {
|
||||
const removed = super.removeInput(name, opt_quiet);
|
||||
|
||||
if (this.rendered) {
|
||||
this.queueRender();
|
||||
// Removing an input will cause the block to change shape.
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
|
||||
this.queueRender();
|
||||
return removed;
|
||||
}
|
||||
|
||||
@@ -1307,23 +1227,13 @@ export class BlockSvg
|
||||
*/
|
||||
override moveNumberedInputBefore(inputIndex: number, refIndex: number) {
|
||||
super.moveNumberedInputBefore(inputIndex, refIndex);
|
||||
|
||||
if (this.rendered) {
|
||||
this.queueRender();
|
||||
// Moving an input will cause the block to change shape.
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
this.queueRender();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override appendInput(input: Input): Input {
|
||||
super.appendInput(input);
|
||||
|
||||
if (this.rendered) {
|
||||
this.queueRender();
|
||||
// Adding an input will cause the block to change shape.
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
this.queueRender();
|
||||
return input;
|
||||
}
|
||||
|
||||
@@ -1377,28 +1287,25 @@ export class BlockSvg
|
||||
* Returns connections originating from this block.
|
||||
*
|
||||
* @param all If true, return all connections even hidden ones.
|
||||
* Otherwise, for a non-rendered block return an empty list, and for a
|
||||
* collapsed block don't return inputs connections.
|
||||
* Otherwise, for a collapsed block don't return inputs connections.
|
||||
* @returns Array of connections.
|
||||
* @internal
|
||||
*/
|
||||
override getConnections_(all: boolean): RenderedConnection[] {
|
||||
const myConnections = [];
|
||||
if (all || this.rendered) {
|
||||
if (this.outputConnection) {
|
||||
myConnections.push(this.outputConnection);
|
||||
}
|
||||
if (this.previousConnection) {
|
||||
myConnections.push(this.previousConnection);
|
||||
}
|
||||
if (this.nextConnection) {
|
||||
myConnections.push(this.nextConnection);
|
||||
}
|
||||
if (all || !this.collapsed_) {
|
||||
for (let i = 0, input; (input = this.inputList[i]); i++) {
|
||||
if (input.connection) {
|
||||
myConnections.push(input.connection as RenderedConnection);
|
||||
}
|
||||
if (this.outputConnection) {
|
||||
myConnections.push(this.outputConnection);
|
||||
}
|
||||
if (this.previousConnection) {
|
||||
myConnections.push(this.previousConnection);
|
||||
}
|
||||
if (this.nextConnection) {
|
||||
myConnections.push(this.nextConnection);
|
||||
}
|
||||
if (all || !this.collapsed_) {
|
||||
for (let i = 0, input; (input = this.inputList[i]); i++) {
|
||||
if (input.connection) {
|
||||
myConnections.push(input.connection as RenderedConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1474,22 +1381,6 @@ export class BlockSvg
|
||||
* up on screen, because that creates confusion for end-users.
|
||||
*/
|
||||
override bumpNeighbours() {
|
||||
if (this.bumpNeighboursPid) return;
|
||||
const group = eventUtils.getGroup();
|
||||
|
||||
this.bumpNeighboursPid = setTimeout(() => {
|
||||
const oldGroup = eventUtils.getGroup();
|
||||
eventUtils.setGroup(group);
|
||||
this.getRootBlock().bumpNeighboursInternal();
|
||||
eventUtils.setGroup(oldGroup);
|
||||
this.bumpNeighboursPid = 0;
|
||||
}, config.bumpDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bumps unconnected blocks out of alignment.
|
||||
*/
|
||||
private bumpNeighboursInternal() {
|
||||
const root = this.getRootBlock();
|
||||
if (
|
||||
this.isDeadOrDying() ||
|
||||
@@ -1506,16 +1397,13 @@ export class BlockSvg
|
||||
for (const conn of this.getConnections_(false)) {
|
||||
if (conn.isSuperior()) {
|
||||
// Recurse down the block stack.
|
||||
conn.targetBlock()?.bumpNeighboursInternal();
|
||||
conn.targetBlock()?.bumpNeighbours();
|
||||
}
|
||||
|
||||
for (const neighbour of conn.neighbours(config.snapRadius)) {
|
||||
// Don't bump away from things that are in our stack.
|
||||
if (neighbourIsInStack(neighbour)) continue;
|
||||
// If both connections are connected, that's fine.
|
||||
if (conn.isConnected() && neighbour.isConnected()) continue;
|
||||
|
||||
// Always bump the inferior connection.
|
||||
if (conn.isSuperior()) {
|
||||
neighbour.bumpAwayFrom(conn);
|
||||
} else {
|
||||
@@ -1526,21 +1414,11 @@ export class BlockSvg
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule snapping to grid and bumping neighbours to occur after a brief
|
||||
* delay.
|
||||
*
|
||||
* @internal
|
||||
* Snap to grid, and then bump neighbouring blocks away at the end of the next
|
||||
* render.
|
||||
*/
|
||||
scheduleSnapAndBump() {
|
||||
// Ensure that any snap and bump are part of this move's event group.
|
||||
const group = eventUtils.getGroup();
|
||||
|
||||
setTimeout(() => {
|
||||
eventUtils.setGroup(group);
|
||||
this.snapToGrid();
|
||||
eventUtils.setGroup(false);
|
||||
}, config.bumpDelay / 2);
|
||||
|
||||
this.snapToGrid();
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
|
||||
@@ -1617,7 +1495,6 @@ export class BlockSvg
|
||||
* @internal
|
||||
*/
|
||||
renderEfficiently() {
|
||||
this.rendered = true;
|
||||
dom.startTextWidthCache();
|
||||
|
||||
if (this.isCollapsed()) {
|
||||
@@ -1741,4 +1618,60 @@ export class BlockSvg
|
||||
add,
|
||||
);
|
||||
}
|
||||
|
||||
/** Sets the drag strategy for this block. */
|
||||
setDragStrategy(dragStrategy: IDragStrategy) {
|
||||
this.dragStrategy = dragStrategy;
|
||||
}
|
||||
|
||||
/** Returns whether this block is movable or not. */
|
||||
override isMovable(): boolean {
|
||||
return this.dragStrategy.isMovable();
|
||||
}
|
||||
|
||||
/** Starts a drag on the block. */
|
||||
startDrag(e?: PointerEvent): void {
|
||||
this.dragStrategy.startDrag(e);
|
||||
}
|
||||
|
||||
/** Drags the block to the given location. */
|
||||
drag(newLoc: Coordinate, e?: PointerEvent): void {
|
||||
this.dragStrategy.drag(newLoc, e);
|
||||
}
|
||||
|
||||
/** Ends the drag on the block. */
|
||||
endDrag(e?: PointerEvent): void {
|
||||
this.dragStrategy.endDrag(e);
|
||||
}
|
||||
|
||||
/** Moves the block back to where it was at the start of a drag. */
|
||||
revertDrag(): void {
|
||||
this.dragStrategy.revertDrag();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a representation of this block that can be displayed in a flyout.
|
||||
*/
|
||||
toFlyoutInfo(): FlyoutItemInfo[] {
|
||||
const json: FlyoutItemInfo = {
|
||||
kind: 'BLOCK',
|
||||
...blocks.save(this),
|
||||
};
|
||||
|
||||
const toRemove = new Set(['id', 'height', 'width', 'pinned', 'enabled']);
|
||||
|
||||
// Traverse the JSON recursively.
|
||||
const traverseJson = function (json: {[key: string]: unknown}) {
|
||||
for (const key in json) {
|
||||
if (toRemove.has(key)) {
|
||||
delete json[key];
|
||||
} else if (typeof json[key] === 'object') {
|
||||
traverseJson(json[key] as {[key: string]: unknown});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
traverseJson(json as unknown as {[key: string]: unknown});
|
||||
return [json];
|
||||
}
|
||||
}
|
||||
|
||||
145
core/blockly.ts
145
core/blockly.ts
@@ -17,14 +17,11 @@ import './events/events_var_create.js';
|
||||
|
||||
import {Block} from './block.js';
|
||||
import * as blockAnimations from './block_animations.js';
|
||||
import {BlockDragger} from './block_dragger.js';
|
||||
import {BlockSvg} from './block_svg.js';
|
||||
import {BlocklyOptions} from './blockly_options.js';
|
||||
import {Blocks} from './blocks.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import {Bubble} from './bubbles/bubble.js';
|
||||
import * as bubbles from './bubbles.js';
|
||||
import {BubbleDragger} from './bubble_dragger.js';
|
||||
import * as bumpObjects from './bump_objects.js';
|
||||
import * as clipboard from './clipboard.js';
|
||||
import * as common from './common.js';
|
||||
@@ -37,9 +34,11 @@ import {ConnectionType} from './connection_type.js';
|
||||
import * as ContextMenu from './contextmenu.js';
|
||||
import * as ContextMenuItems from './contextmenu_items.js';
|
||||
import {ContextMenuRegistry} from './contextmenu_registry.js';
|
||||
import * as comments from './comments.js';
|
||||
import * as Css from './css.js';
|
||||
import {DeleteArea} from './delete_area.js';
|
||||
import * as dialog from './dialog.js';
|
||||
import * as dragging from './dragging.js';
|
||||
import {DragTarget} from './drag_target.js';
|
||||
import * as dropDownDiv from './dropdowndiv.js';
|
||||
import * as Events from './events/events.js';
|
||||
@@ -50,24 +49,12 @@ import {
|
||||
FieldValidator,
|
||||
UnattachedFieldError,
|
||||
} from './field.js';
|
||||
import {
|
||||
FieldAngle,
|
||||
FieldAngleConfig,
|
||||
FieldAngleFromJsonConfig,
|
||||
FieldAngleValidator,
|
||||
} from './field_angle.js';
|
||||
import {
|
||||
FieldCheckbox,
|
||||
FieldCheckboxConfig,
|
||||
FieldCheckboxFromJsonConfig,
|
||||
FieldCheckboxValidator,
|
||||
} from './field_checkbox.js';
|
||||
import {
|
||||
FieldColour,
|
||||
FieldColourConfig,
|
||||
FieldColourFromJsonConfig,
|
||||
FieldColourValidator,
|
||||
} from './field_colour.js';
|
||||
import {
|
||||
FieldDropdown,
|
||||
FieldDropdownConfig,
|
||||
@@ -88,12 +75,6 @@ import {
|
||||
FieldLabelFromJsonConfig,
|
||||
} from './field_label.js';
|
||||
import {FieldLabelSerializable} from './field_label_serializable.js';
|
||||
import {
|
||||
FieldMultilineInput,
|
||||
FieldMultilineInputConfig,
|
||||
FieldMultilineInputFromJsonConfig,
|
||||
FieldMultilineInputValidator,
|
||||
} from './field_multilineinput.js';
|
||||
import {
|
||||
FieldNumber,
|
||||
FieldNumberConfig,
|
||||
@@ -123,9 +104,7 @@ import {Gesture} from './gesture.js';
|
||||
import {Grid} from './grid.js';
|
||||
import * as icons from './icons.js';
|
||||
import {inject} from './inject.js';
|
||||
import {Align} from './inputs/align.js';
|
||||
import {Input} from './inputs/input.js';
|
||||
import {inputTypes} from './inputs/input_types.js';
|
||||
import * as inputs from './inputs.js';
|
||||
import {InsertionMarkerManager} from './insertion_marker_manager.js';
|
||||
import {InsertionMarkerPreviewer} from './insertion_marker_previewer.js';
|
||||
@@ -133,7 +112,6 @@ import {IASTNodeLocation} from './interfaces/i_ast_node_location.js';
|
||||
import {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
|
||||
import {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js';
|
||||
import {IAutoHideable} from './interfaces/i_autohideable.js';
|
||||
import {IBlockDragger} from './interfaces/i_block_dragger.js';
|
||||
import {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||
import {IBubble} from './interfaces/i_bubble.js';
|
||||
import {ICollapsibleToolboxItem} from './interfaces/i_collapsible_toolbox_item.js';
|
||||
@@ -141,11 +119,16 @@ import {IComponent} from './interfaces/i_component.js';
|
||||
import {IConnectionChecker} from './interfaces/i_connection_checker.js';
|
||||
import {IConnectionPreviewer} from './interfaces/i_connection_previewer.js';
|
||||
import {IContextMenu} from './interfaces/i_contextmenu.js';
|
||||
import {ICopyable, isCopyable} from './interfaces/i_copyable.js';
|
||||
import {IDeletable} from './interfaces/i_deletable.js';
|
||||
import {ICopyable, isCopyable, ICopyData} from './interfaces/i_copyable.js';
|
||||
import {IDeletable, isDeletable} from './interfaces/i_deletable.js';
|
||||
import {IDeleteArea} from './interfaces/i_delete_area.js';
|
||||
import {IDragTarget} from './interfaces/i_drag_target.js';
|
||||
import {IDraggable} from './interfaces/i_draggable.js';
|
||||
import {IDragger} from './interfaces/i_dragger.js';
|
||||
import {
|
||||
IDraggable,
|
||||
isDraggable,
|
||||
IDragStrategy,
|
||||
} from './interfaces/i_draggable.js';
|
||||
import {IFlyout} from './interfaces/i_flyout.js';
|
||||
import {IHasBubble, hasBubble} from './interfaces/i_has_bubble.js';
|
||||
import {IIcon, isIcon} from './interfaces/i_icon.js';
|
||||
@@ -156,7 +139,11 @@ import {IObservable, isObservable} from './interfaces/i_observable.js';
|
||||
import {IPaster, isPaster} from './interfaces/i_paster.js';
|
||||
import {IPositionable} from './interfaces/i_positionable.js';
|
||||
import {IRegistrable} from './interfaces/i_registrable.js';
|
||||
import {ISelectable} from './interfaces/i_selectable.js';
|
||||
import {
|
||||
IRenderedElement,
|
||||
isRenderedElement,
|
||||
} from './interfaces/i_rendered_element.js';
|
||||
import {ISelectable, isSelectable} from './interfaces/i_selectable.js';
|
||||
import {ISelectableToolboxItem} from './interfaces/i_selectable_toolbox_item.js';
|
||||
import {ISerializable, isSerializable} from './interfaces/i_serializable.js';
|
||||
import {IStyleable} from './interfaces/i_styleable.js';
|
||||
@@ -173,6 +160,7 @@ import {Cursor} from './keyboard_nav/cursor.js';
|
||||
import {Marker} from './keyboard_nav/marker.js';
|
||||
import {TabNavigateCursor} from './keyboard_nav/tab_navigate_cursor.js';
|
||||
import {MarkerManager} from './marker_manager.js';
|
||||
import type {LayerManager} from './layer_manager.js';
|
||||
import {Menu} from './menu.js';
|
||||
import {MenuItem} from './menuitem.js';
|
||||
import {MetricsManager} from './metrics_manager.js';
|
||||
@@ -188,7 +176,6 @@ import * as renderManagement from './render_management.js';
|
||||
import * as blockRendering from './renderers/common/block_rendering.js';
|
||||
import * as constants from './constants.js';
|
||||
import * as geras from './renderers/geras/geras.js';
|
||||
import * as minimalist from './renderers/minimalist/minimalist.js';
|
||||
import * as thrasos from './renderers/thrasos/thrasos.js';
|
||||
import * as zelos from './renderers/zelos/zelos.js';
|
||||
import {Scrollbar} from './scrollbar.js';
|
||||
@@ -216,8 +203,6 @@ import * as VariablesDynamic from './variables_dynamic.js';
|
||||
import * as WidgetDiv from './widgetdiv.js';
|
||||
import {Workspace} from './workspace.js';
|
||||
import {WorkspaceAudio} from './workspace_audio.js';
|
||||
import {WorkspaceComment} from './workspace_comment.js';
|
||||
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
|
||||
import {WorkspaceDragger} from './workspace_dragger.js';
|
||||
import {WorkspaceSvg} from './workspace_svg.js';
|
||||
import * as Xml from './xml.js';
|
||||
@@ -243,27 +228,6 @@ export const VERSION = 'uncompiled';
|
||||
* namespace to put new functions on.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Aliases for input alignments used in block defintions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @see Blockly.Input.Align.LEFT
|
||||
* @deprecated Use `Blockly.inputs.Align.LEFT`. To be removed in v11.
|
||||
*/
|
||||
export const ALIGN_LEFT = Align.LEFT;
|
||||
|
||||
/**
|
||||
* @see Blockly.Input.Align.CENTRE
|
||||
* @deprecated Use `Blockly.inputs.Align.CENTER`. To be removed in v11.
|
||||
*/
|
||||
export const ALIGN_CENTRE = Align.CENTRE;
|
||||
|
||||
/**
|
||||
* @see Blockly.Input.Align.RIGHT
|
||||
* @deprecated Use `Blockly.inputs.Align.RIGHT`. To be removed in v11.
|
||||
*/
|
||||
export const ALIGN_RIGHT = Align.RIGHT;
|
||||
/*
|
||||
* Aliases for constants used for connection and input types.
|
||||
*/
|
||||
@@ -288,12 +252,6 @@ export const NEXT_STATEMENT = ConnectionType.NEXT_STATEMENT;
|
||||
*/
|
||||
export const PREVIOUS_STATEMENT = ConnectionType.PREVIOUS_STATEMENT;
|
||||
|
||||
/**
|
||||
* @see inputTypes.DUMMY_INPUT
|
||||
* @deprecated Use `Blockly.inputs.inputTypes.DUMMY`. To be removed in v11.
|
||||
*/
|
||||
export const DUMMY_INPUT = inputTypes.DUMMY;
|
||||
|
||||
/** Aliases for toolbox positions. */
|
||||
|
||||
/**
|
||||
@@ -378,7 +336,6 @@ export const setParentContainer = common.setParentContainer;
|
||||
|
||||
// Aliases to allow external code to access these values for legacy reasons.
|
||||
export const COLLAPSE_CHARS = internalConstants.COLLAPSE_CHARS;
|
||||
export const DRAG_STACK = internalConstants.DRAG_STACK;
|
||||
export const OPPOSITE_TYPE = internalConstants.OPPOSITE_TYPE;
|
||||
export const RENAME_VARIABLE_ID = internalConstants.RENAME_VARIABLE_ID;
|
||||
export const DELETE_VARIABLE_ID = internalConstants.DELETE_VARIABLE_ID;
|
||||
@@ -424,25 +381,20 @@ WorkspaceSvg.prototype.newBlock = function (
|
||||
return new BlockSvg(this, prototypeName, opt_id);
|
||||
};
|
||||
|
||||
WorkspaceSvg.newTrashcan = function (workspace: WorkspaceSvg): Trashcan {
|
||||
return new Trashcan(workspace);
|
||||
Workspace.prototype.newComment = function (
|
||||
id?: string,
|
||||
): comments.WorkspaceComment {
|
||||
return new comments.WorkspaceComment(this, id);
|
||||
};
|
||||
|
||||
WorkspaceCommentSvg.prototype.showContextMenu = function (
|
||||
this: WorkspaceCommentSvg,
|
||||
e: Event,
|
||||
) {
|
||||
if (this.workspace.options.readOnly) {
|
||||
return;
|
||||
}
|
||||
const menuOptions = [];
|
||||
WorkspaceSvg.prototype.newComment = function (
|
||||
id?: string,
|
||||
): comments.RenderedWorkspaceComment {
|
||||
return new comments.RenderedWorkspaceComment(this, id);
|
||||
};
|
||||
|
||||
if (this.isDeletable() && this.isMovable()) {
|
||||
menuOptions.push(ContextMenu.commentDuplicateOption(this));
|
||||
menuOptions.push(ContextMenu.commentDeleteOption(this));
|
||||
}
|
||||
|
||||
ContextMenu.show(e, menuOptions, this.RTL);
|
||||
WorkspaceSvg.newTrashcan = function (workspace: WorkspaceSvg): Trashcan {
|
||||
return new Trashcan(workspace);
|
||||
};
|
||||
|
||||
MiniWorkspaceBubble.prototype.newWorkspaceSvg = function (
|
||||
@@ -490,7 +442,6 @@ export {constants};
|
||||
export {dialog};
|
||||
export {fieldRegistry};
|
||||
export {geras};
|
||||
export {minimalist};
|
||||
export {registry};
|
||||
export {thrasos};
|
||||
export {uiPosition};
|
||||
@@ -500,13 +451,9 @@ export {ASTNode};
|
||||
export {BasicCursor};
|
||||
export {Block};
|
||||
export {BlocklyOptions};
|
||||
export {BlockDragger};
|
||||
export {BlockSvg};
|
||||
export {Blocks};
|
||||
export {bubbles};
|
||||
/** @deprecated Use Blockly.bubbles.Bubble instead. To be removed in v11. */
|
||||
export {Bubble};
|
||||
export {BubbleDragger};
|
||||
export {CollapsibleToolboxCategory};
|
||||
export {ComponentManager};
|
||||
export {Connection};
|
||||
@@ -514,29 +461,19 @@ export {ConnectionType};
|
||||
export {ConnectionChecker};
|
||||
export {ConnectionDB};
|
||||
export {ContextMenuRegistry};
|
||||
export {comments};
|
||||
export {Cursor};
|
||||
export {DeleteArea};
|
||||
export {dragging};
|
||||
export {DragTarget};
|
||||
export const DropDownDiv = dropDownDiv;
|
||||
export {Field, FieldConfig, FieldValidator, UnattachedFieldError};
|
||||
export {
|
||||
FieldAngle,
|
||||
FieldAngleConfig,
|
||||
FieldAngleFromJsonConfig,
|
||||
FieldAngleValidator,
|
||||
};
|
||||
export {
|
||||
FieldCheckbox,
|
||||
FieldCheckboxConfig,
|
||||
FieldCheckboxFromJsonConfig,
|
||||
FieldCheckboxValidator,
|
||||
};
|
||||
export {
|
||||
FieldColour,
|
||||
FieldColourConfig,
|
||||
FieldColourFromJsonConfig,
|
||||
FieldColourValidator,
|
||||
};
|
||||
export {
|
||||
FieldDropdown,
|
||||
FieldDropdownConfig,
|
||||
@@ -549,12 +486,6 @@ export {
|
||||
export {FieldImage, FieldImageConfig, FieldImageFromJsonConfig};
|
||||
export {FieldLabel, FieldLabelConfig, FieldLabelFromJsonConfig};
|
||||
export {FieldLabelSerializable};
|
||||
export {
|
||||
FieldMultilineInput,
|
||||
FieldMultilineInputConfig,
|
||||
FieldMultilineInputFromJsonConfig,
|
||||
FieldMultilineInputValidator,
|
||||
};
|
||||
export {
|
||||
FieldNumber,
|
||||
FieldNumberConfig,
|
||||
@@ -585,7 +516,6 @@ export {IASTNodeLocation};
|
||||
export {IASTNodeLocationSvg};
|
||||
export {IASTNodeLocationWithBlock};
|
||||
export {IAutoHideable};
|
||||
export {IBlockDragger};
|
||||
export {IBoundedElement};
|
||||
export {IBubble};
|
||||
export {ICollapsibleToolboxItem};
|
||||
@@ -594,11 +524,12 @@ export {IConnectionChecker};
|
||||
export {IConnectionPreviewer};
|
||||
export {IContextMenu};
|
||||
export {icons};
|
||||
export {ICopyable, isCopyable};
|
||||
export {IDeletable};
|
||||
export {ICopyable, isCopyable, ICopyData};
|
||||
export {IDeletable, isDeletable};
|
||||
export {IDeleteArea};
|
||||
export {IDragTarget};
|
||||
export {IDraggable};
|
||||
export {IDragger};
|
||||
export {IDraggable, isDraggable, IDragStrategy};
|
||||
export {IFlyout};
|
||||
export {IHasBubble, hasBubble};
|
||||
export {IIcon, isIcon};
|
||||
@@ -613,7 +544,8 @@ export {IObservable, isObservable};
|
||||
export {IPaster, isPaster};
|
||||
export {IPositionable};
|
||||
export {IRegistrable};
|
||||
export {ISelectable};
|
||||
export {IRenderedElement, isRenderedElement};
|
||||
export {ISelectable, isSelectable};
|
||||
export {ISelectableToolboxItem};
|
||||
export {ISerializable, isSerializable};
|
||||
export {IStyleable};
|
||||
@@ -622,6 +554,7 @@ export {IToolboxItem};
|
||||
export {IVariableBackedParameterModel, isVariableBackedParameterModel};
|
||||
export {Marker};
|
||||
export {MarkerManager};
|
||||
export {LayerManager};
|
||||
export {Menu};
|
||||
export {MenuItem};
|
||||
export {MetricsManager};
|
||||
@@ -646,15 +579,9 @@ export {VariableModel};
|
||||
export {VerticalFlyout};
|
||||
export {Workspace};
|
||||
export {WorkspaceAudio};
|
||||
export {WorkspaceComment};
|
||||
export {WorkspaceCommentSvg};
|
||||
export {WorkspaceDragger};
|
||||
export {WorkspaceSvg};
|
||||
export {ZoomControls};
|
||||
export {config};
|
||||
/** @deprecated Use Blockly.ConnectionType instead. */
|
||||
export const connectionTypes = ConnectionType;
|
||||
export {inject};
|
||||
/** @deprecated Use Blockly.inputs.inputTypes instead. To be removed in v11. */
|
||||
export {inputTypes};
|
||||
export {serialization};
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2018 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Methods for dragging a bubble visually.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
// Former goog.module ID: Blockly.BubbleDragger
|
||||
|
||||
import {ComponentManager} from './component_manager.js';
|
||||
import type {CommentMove} from './events/events_comment_move.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import type {IBubble} from './interfaces/i_bubble.js';
|
||||
import type {IDeleteArea} from './interfaces/i_delete_area.js';
|
||||
import type {IDragTarget} from './interfaces/i_drag_target.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import * as layers from './layers.js';
|
||||
|
||||
/**
|
||||
* Class for a bubble dragger. It moves things on the bubble canvas around the
|
||||
* workspace when they are being dragged by a mouse or touch. These can be
|
||||
* block comments, mutators, warnings, or workspace comments.
|
||||
*/
|
||||
export class BubbleDragger {
|
||||
/** Which drag target the mouse pointer is over, if any. */
|
||||
private dragTarget_: IDragTarget | null = null;
|
||||
|
||||
/** Whether the bubble would be deleted if dropped immediately. */
|
||||
private wouldDeleteBubble_ = false;
|
||||
private readonly startXY_: Coordinate;
|
||||
|
||||
/**
|
||||
* @param bubble The item on the bubble canvas to drag.
|
||||
* @param workspace The workspace to drag on.
|
||||
*/
|
||||
constructor(
|
||||
private bubble: IBubble,
|
||||
private workspace: WorkspaceSvg,
|
||||
) {
|
||||
/**
|
||||
* The location of the top left corner of the dragging bubble's body at the
|
||||
* beginning of the drag, in workspace coordinates.
|
||||
*/
|
||||
this.startXY_ = this.bubble.getRelativeToSurfaceXY();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start dragging a bubble.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
startBubbleDrag() {
|
||||
if (!eventUtils.getGroup()) {
|
||||
eventUtils.setGroup(true);
|
||||
}
|
||||
|
||||
this.workspace.setResizesEnabled(false);
|
||||
if ((this.bubble as AnyDuringMigration).setAutoLayout) {
|
||||
(this.bubble as AnyDuringMigration).setAutoLayout(false);
|
||||
}
|
||||
|
||||
this.workspace.getLayerManager()?.moveToDragLayer(this.bubble);
|
||||
|
||||
this.bubble.setDragging && this.bubble.setDragging(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a step of bubble dragging, based on the given event. Update the
|
||||
* display accordingly.
|
||||
*
|
||||
* @param e The most recent move event.
|
||||
* @param currentDragDeltaXY How far the pointer has moved from the position
|
||||
* at the start of the drag, in pixel units.
|
||||
* @internal
|
||||
*/
|
||||
dragBubble(e: PointerEvent, currentDragDeltaXY: Coordinate) {
|
||||
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
|
||||
const newLoc = Coordinate.sum(this.startXY_, delta);
|
||||
this.bubble.moveDuringDrag(newLoc);
|
||||
|
||||
const oldDragTarget = this.dragTarget_;
|
||||
this.dragTarget_ = this.workspace.getDragTarget(e);
|
||||
|
||||
const oldWouldDeleteBubble = this.wouldDeleteBubble_;
|
||||
this.wouldDeleteBubble_ = this.shouldDelete_(this.dragTarget_);
|
||||
if (oldWouldDeleteBubble !== this.wouldDeleteBubble_) {
|
||||
// Prevent unnecessary add/remove class calls.
|
||||
this.updateCursorDuringBubbleDrag_();
|
||||
}
|
||||
// Call drag enter/exit/over after wouldDeleteBlock is called in
|
||||
// shouldDelete_
|
||||
if (this.dragTarget_ !== oldDragTarget) {
|
||||
oldDragTarget && oldDragTarget.onDragExit(this.bubble);
|
||||
this.dragTarget_ && this.dragTarget_.onDragEnter(this.bubble);
|
||||
}
|
||||
this.dragTarget_ && this.dragTarget_.onDragOver(this.bubble);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether ending the drag would delete the bubble.
|
||||
*
|
||||
* @param dragTarget The drag target that the bubblee is currently over.
|
||||
* @returns Whether dropping the bubble immediately would delete the block.
|
||||
*/
|
||||
private shouldDelete_(dragTarget: IDragTarget | null): boolean {
|
||||
if (dragTarget) {
|
||||
const componentManager = this.workspace.getComponentManager();
|
||||
const isDeleteArea = componentManager.hasCapability(
|
||||
dragTarget.id,
|
||||
ComponentManager.Capability.DELETE_AREA,
|
||||
);
|
||||
if (isDeleteArea) {
|
||||
return (dragTarget as IDeleteArea).wouldDelete(this.bubble, false);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cursor (and possibly the trash can lid) to reflect whether the
|
||||
* dragging bubble would be deleted if released immediately.
|
||||
*/
|
||||
private updateCursorDuringBubbleDrag_() {
|
||||
this.bubble.setDeleteStyle(this.wouldDeleteBubble_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish a bubble drag and put the bubble back on the workspace.
|
||||
*
|
||||
* @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: PointerEvent, currentDragDeltaXY: Coordinate) {
|
||||
// Make sure internal state is fresh.
|
||||
this.dragBubble(e, currentDragDeltaXY);
|
||||
|
||||
const preventMove =
|
||||
this.dragTarget_ && this.dragTarget_.shouldPreventMove(this.bubble);
|
||||
let newLoc;
|
||||
if (preventMove) {
|
||||
newLoc = this.startXY_;
|
||||
} else {
|
||||
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
|
||||
newLoc = Coordinate.sum(this.startXY_, delta);
|
||||
}
|
||||
// Move the bubble to its final location.
|
||||
this.bubble.moveTo(newLoc.x, newLoc.y);
|
||||
|
||||
if (this.dragTarget_) {
|
||||
this.dragTarget_.onDrop(this.bubble);
|
||||
}
|
||||
|
||||
if (this.wouldDeleteBubble_) {
|
||||
// Fire a move event, so we know where to go back to for an undo.
|
||||
this.fireMoveEvent_();
|
||||
this.bubble.dispose();
|
||||
} else {
|
||||
// Put everything back onto the bubble canvas.
|
||||
if (this.bubble.setDragging) {
|
||||
this.bubble.setDragging(false);
|
||||
this.workspace
|
||||
.getLayerManager()
|
||||
?.moveOffDragLayer(this.bubble, layers.BUBBLE);
|
||||
}
|
||||
this.fireMoveEvent_();
|
||||
}
|
||||
this.workspace.setResizesEnabled(true);
|
||||
|
||||
eventUtils.setGroup(false);
|
||||
}
|
||||
|
||||
/** Fire a move event at the end of a bubble drag. */
|
||||
private fireMoveEvent_() {
|
||||
if (this.bubble instanceof WorkspaceCommentSvg) {
|
||||
const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(
|
||||
this.bubble,
|
||||
) as CommentMove;
|
||||
event.setOldCoordinate(this.startXY_);
|
||||
event.recordNew();
|
||||
eventUtils.fire(event);
|
||||
}
|
||||
// TODO (fenichel): move events for comments.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a coordinate object from pixels to workspace units, including a
|
||||
* correction for mutator workspaces.
|
||||
* This function does not consider differing origins. It simply scales the
|
||||
* input's x and y values.
|
||||
*
|
||||
* @param pixelCoord A coordinate with x and y values in CSS pixel units.
|
||||
* @returns The input coordinate divided by the workspace scale.
|
||||
*/
|
||||
private pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate {
|
||||
const result = new Coordinate(
|
||||
pixelCoord.x / this.workspace.scale,
|
||||
pixelCoord.y / this.workspace.scale,
|
||||
);
|
||||
if (this.workspace.isMutator) {
|
||||
// If we're in a mutator, its scale is always 1, purely because of some
|
||||
// oddities in our rendering optimizations. The actual scale is the same
|
||||
// as the scale on the parent workspace. Fix that for dragging.
|
||||
const mainScale = this.workspace.options.parentWorkspace!.scale;
|
||||
result.scale(1 / mainScale);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import * as browserEvents from '../browser_events.js';
|
||||
import {BubbleDragStrategy} from '../dragging/bubble_drag_strategy.js';
|
||||
import {IBubble} from '../interfaces/i_bubble.js';
|
||||
import {ContainerRegion} from '../metrics_manager.js';
|
||||
import {Scrollbar} from '../scrollbar.js';
|
||||
@@ -15,13 +16,16 @@ import {Rect} from '../utils/rect.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import * as common from '../common.js';
|
||||
import {ISelectable} from '../blockly.js';
|
||||
import * as idGenerator from '../utils/idgenerator.js';
|
||||
|
||||
/**
|
||||
* The abstract pop-up bubble class. This creates a UI that looks like a speech
|
||||
* bubble, where it has a "tail" that points to the block, and a "head" that
|
||||
* displays arbitrary svg elements.
|
||||
*/
|
||||
export abstract class Bubble implements IBubble {
|
||||
export abstract class Bubble implements IBubble, ISelectable {
|
||||
/** The width of the border around the bubble. */
|
||||
static readonly BORDER_WIDTH = 6;
|
||||
|
||||
@@ -49,6 +53,8 @@ export abstract class Bubble implements IBubble {
|
||||
/** Distance between arrow point and anchor point. */
|
||||
static readonly ANCHOR_RADIUS = 8;
|
||||
|
||||
public id: string;
|
||||
|
||||
/** The SVG group containing all parts of the bubble. */
|
||||
protected svgRoot: SVGGElement;
|
||||
|
||||
@@ -78,6 +84,8 @@ export abstract class Bubble implements IBubble {
|
||||
/** The position of the left of the bubble realtive to its anchor. */
|
||||
private relativeLeft = 0;
|
||||
|
||||
private dragStrategy = new BubbleDragStrategy(this, this.workspace);
|
||||
|
||||
/**
|
||||
* @param workspace The workspace this bubble belongs to.
|
||||
* @param anchor The anchor location of the thing this bubble is attached to.
|
||||
@@ -86,11 +94,16 @@ export abstract class Bubble implements IBubble {
|
||||
* when automatically positioning.
|
||||
*/
|
||||
constructor(
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
public readonly workspace: WorkspaceSvg,
|
||||
protected anchor: Coordinate,
|
||||
protected ownerRect?: Rect,
|
||||
) {
|
||||
this.svgRoot = dom.createSvgElement(Svg.G, {}, workspace.getBubbleCanvas());
|
||||
this.id = idGenerator.getNextUniqueId();
|
||||
this.svgRoot = dom.createSvgElement(
|
||||
Svg.G,
|
||||
{'class': 'blocklyBubble'},
|
||||
workspace.getBubbleCanvas(),
|
||||
);
|
||||
const embossGroup = dom.createSvgElement(
|
||||
Svg.G,
|
||||
{
|
||||
@@ -100,7 +113,11 @@ export abstract class Bubble implements IBubble {
|
||||
},
|
||||
this.svgRoot,
|
||||
);
|
||||
this.tail = dom.createSvgElement(Svg.PATH, {}, embossGroup);
|
||||
this.tail = dom.createSvgElement(
|
||||
Svg.PATH,
|
||||
{'class': 'blocklyBubbleTail'},
|
||||
embossGroup,
|
||||
);
|
||||
this.background = dom.createSvgElement(
|
||||
Svg.RECT,
|
||||
{
|
||||
@@ -198,6 +215,7 @@ export abstract class Bubble implements IBubble {
|
||||
/** Passes the pointer event off to the gesture system. */
|
||||
private onMouseDown(e: PointerEvent) {
|
||||
this.workspace.getGesture(e)?.handleBubbleStart(e, this);
|
||||
common.setSelected(this);
|
||||
}
|
||||
|
||||
/** Positions the bubble relative to its anchor. Does not render its tail. */
|
||||
@@ -604,4 +622,37 @@ export abstract class Bubble implements IBubble {
|
||||
showContextMenu(_e: Event) {
|
||||
// NOOP in base class.
|
||||
}
|
||||
|
||||
/** Returns whether this bubble is movable or not. */
|
||||
isMovable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Starts a drag on the bubble. */
|
||||
startDrag(): void {
|
||||
this.dragStrategy.startDrag();
|
||||
}
|
||||
|
||||
/** Drags the bubble to the given location. */
|
||||
drag(newLoc: Coordinate): void {
|
||||
this.dragStrategy.drag(newLoc);
|
||||
}
|
||||
|
||||
/** Ends the drag on the bubble. */
|
||||
endDrag(): void {
|
||||
this.dragStrategy.endDrag();
|
||||
}
|
||||
|
||||
/** Moves the bubble back to where it was at the start of a drag. */
|
||||
revertDrag(): void {
|
||||
this.dragStrategy.revertDrag();
|
||||
}
|
||||
|
||||
select(): void {
|
||||
// Bubbles don't have any visual for being selected.
|
||||
}
|
||||
|
||||
unselect(): void {
|
||||
// Bubbles don't have any visual for being selected.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export class MiniWorkspaceBubble extends Bubble {
|
||||
/** @internal */
|
||||
constructor(
|
||||
workspaceOptions: BlocklyOptions,
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
public readonly workspace: WorkspaceSvg,
|
||||
protected anchor: Coordinate,
|
||||
protected ownerRect?: Rect,
|
||||
) {
|
||||
|
||||
@@ -20,7 +20,7 @@ export class TextBubble extends Bubble {
|
||||
|
||||
constructor(
|
||||
private text: string,
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
public readonly workspace: WorkspaceSvg,
|
||||
protected anchor: Coordinate,
|
||||
protected ownerRect?: Rect,
|
||||
) {
|
||||
|
||||
@@ -70,15 +70,16 @@ export class TextInputBubble extends Bubble {
|
||||
* when automatically positioning.
|
||||
*/
|
||||
constructor(
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
public readonly workspace: WorkspaceSvg,
|
||||
protected anchor: Coordinate,
|
||||
protected ownerRect?: Rect,
|
||||
) {
|
||||
super(workspace, anchor, ownerRect);
|
||||
dom.addClass(this.svgRoot, 'blocklyTextInputBubble');
|
||||
({inputRoot: this.inputRoot, textArea: this.textArea} = this.createEditor(
|
||||
this.contentContainer,
|
||||
));
|
||||
this.resizeGroup = this.createResizeHandle(this.svgRoot);
|
||||
this.resizeGroup = this.createResizeHandle(this.svgRoot, workspace);
|
||||
this.setSize(this.DEFAULT_SIZE, true);
|
||||
}
|
||||
|
||||
@@ -126,7 +127,7 @@ export class TextInputBubble extends Bubble {
|
||||
dom.HTML_NS,
|
||||
'textarea',
|
||||
) as HTMLTextAreaElement;
|
||||
textArea.className = 'blocklyCommentTextarea';
|
||||
textArea.className = 'blocklyTextarea blocklyText';
|
||||
textArea.setAttribute('dir', this.workspace.RTL ? 'RTL' : 'LTR');
|
||||
|
||||
body.appendChild(textArea);
|
||||
@@ -158,51 +159,27 @@ export class TextInputBubble extends Bubble {
|
||||
}
|
||||
|
||||
/** Creates the resize handler elements and binds events to them. */
|
||||
private createResizeHandle(container: SVGGElement): SVGGElement {
|
||||
const resizeGroup = dom.createSvgElement(
|
||||
Svg.G,
|
||||
private createResizeHandle(
|
||||
container: SVGGElement,
|
||||
workspace: WorkspaceSvg,
|
||||
): SVGGElement {
|
||||
const resizeHandle = dom.createSvgElement(
|
||||
Svg.IMAGE,
|
||||
{
|
||||
'class': this.workspace.RTL ? 'blocklyResizeSW' : 'blocklyResizeSE',
|
||||
'class': 'blocklyResizeHandle',
|
||||
'href': `${workspace.options.pathToMedia}resize-handle.svg`,
|
||||
},
|
||||
container,
|
||||
);
|
||||
const size = 2 * Bubble.BORDER_WIDTH;
|
||||
dom.createSvgElement(
|
||||
Svg.POLYGON,
|
||||
{'points': `0,${size} ${size},${size} ${size},0`},
|
||||
resizeGroup,
|
||||
);
|
||||
dom.createSvgElement(
|
||||
Svg.LINE,
|
||||
{
|
||||
'class': 'blocklyResizeLine',
|
||||
'x1': size / 3,
|
||||
'y1': size - 1,
|
||||
'x2': size - 1,
|
||||
'y2': size / 3,
|
||||
},
|
||||
resizeGroup,
|
||||
);
|
||||
dom.createSvgElement(
|
||||
Svg.LINE,
|
||||
{
|
||||
'class': 'blocklyResizeLine',
|
||||
'x1': (size * 2) / 3,
|
||||
'y1': size - 1,
|
||||
'x2': size - 1,
|
||||
'y2': (size * 2) / 3,
|
||||
},
|
||||
resizeGroup,
|
||||
);
|
||||
|
||||
browserEvents.conditionalBind(
|
||||
resizeGroup,
|
||||
resizeHandle,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.onResizePointerDown,
|
||||
);
|
||||
|
||||
return resizeGroup;
|
||||
return resizeHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -330,8 +307,8 @@ export class TextInputBubble extends Bubble {
|
||||
}
|
||||
|
||||
Css.register(`
|
||||
.blocklyCommentTextarea {
|
||||
background-color: #fef49c;
|
||||
.blocklyTextInputBubble .blocklyTextarea {
|
||||
background-color: var(--commentFillColour);
|
||||
border: 0;
|
||||
display: block;
|
||||
margin: 0;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
// Former goog.module ID: Blockly.bumpObjects
|
||||
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
|
||||
import type {Abstract} from './events/events_abstract.js';
|
||||
import type {BlockCreate} from './events/events_block_create.js';
|
||||
import type {BlockMove} from './events/events_block_move.js';
|
||||
@@ -17,7 +17,6 @@ import * as eventUtils from './events/utils.js';
|
||||
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||
import type {ContainerRegion} from './metrics_manager.js';
|
||||
import * as mathUtils from './utils/math.js';
|
||||
import type {WorkspaceCommentSvg} from './workspace_comment_svg.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
/**
|
||||
@@ -152,7 +151,7 @@ export function bumpIntoBoundsHandler(
|
||||
function extractObjectFromEvent(
|
||||
workspace: WorkspaceSvg,
|
||||
e: eventUtils.BumpEvent,
|
||||
): BlockSvg | null | WorkspaceCommentSvg {
|
||||
): IBoundedElement | null {
|
||||
let object = null;
|
||||
switch (e.type) {
|
||||
case eventUtils.BLOCK_CREATE:
|
||||
@@ -166,7 +165,7 @@ function extractObjectFromEvent(
|
||||
case eventUtils.COMMENT_MOVE:
|
||||
object = workspace.getCommentById(
|
||||
(e as CommentCreate | CommentMove).commentId!,
|
||||
) as WorkspaceCommentSvg | null;
|
||||
) as RenderedWorkspaceComment;
|
||||
break;
|
||||
}
|
||||
return object;
|
||||
|
||||
@@ -12,30 +12,12 @@ import * as globalRegistry from './registry.js';
|
||||
import {WorkspaceSvg} from './workspace_svg.js';
|
||||
import * as registry from './clipboard/registry.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import * as deprecation from './utils/deprecation.js';
|
||||
|
||||
/** Metadata about the object that is currently on the clipboard. */
|
||||
let stashedCopyData: ICopyData | null = null;
|
||||
|
||||
let stashedWorkspace: WorkspaceSvg | null = null;
|
||||
|
||||
/**
|
||||
* Copy a copyable element onto the local clipboard.
|
||||
*
|
||||
* @param toCopy The copyable element to be copied.
|
||||
* @deprecated v11. Use `myCopyable.toCopyData()` instead. To be removed v12.
|
||||
* @internal
|
||||
*/
|
||||
export function copy<T extends ICopyData>(toCopy: ICopyable<T>): T | null {
|
||||
deprecation.warn(
|
||||
'Blockly.clipboard.copy',
|
||||
'v11',
|
||||
'v12',
|
||||
'myCopyable.toCopyData()',
|
||||
);
|
||||
return TEST_ONLY.copyInternal(toCopy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private version of copy for stubbing in tests.
|
||||
*/
|
||||
@@ -107,29 +89,6 @@ function pasteFromData<T extends ICopyData>(
|
||||
?.paste(copyData, workspace, coordinate) ?? null) as ICopyable<T> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate this copy-paste-able element.
|
||||
*
|
||||
* @param toDuplicate The element to be duplicated.
|
||||
* @returns The element that was duplicated, or null if the duplication failed.
|
||||
* @deprecated v11. Use
|
||||
* `Blockly.clipboard.paste(myCopyable.toCopyData(), myWorkspace)` instead.
|
||||
* To be removed v12.
|
||||
* @internal
|
||||
*/
|
||||
export function duplicate<
|
||||
U extends ICopyData,
|
||||
T extends ICopyable<U> & IHasWorkspace,
|
||||
>(toDuplicate: T): T | null {
|
||||
deprecation.warn(
|
||||
'Blockly.clipboard.duplicate',
|
||||
'v11',
|
||||
'v12',
|
||||
'Blockly.clipboard.paste(myCopyable.toCopyData(), myWorkspace)',
|
||||
);
|
||||
return TEST_ONLY.duplicateInternal(toDuplicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private version of duplicate for stubbing in tests.
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,7 @@ import {Coordinate} from '../utils/coordinate.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import * as eventUtils from '../events/utils.js';
|
||||
import {config} from '../config.js';
|
||||
import * as common from '../common.js';
|
||||
|
||||
export class BlockPaster implements IPaster<BlockCopyData, BlockSvg> {
|
||||
static TYPE = 'block';
|
||||
@@ -43,7 +44,7 @@ export class BlockPaster implements IPaster<BlockCopyData, BlockSvg> {
|
||||
if (eventUtils.isEnabled() && !block.isShadow()) {
|
||||
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));
|
||||
}
|
||||
block.select();
|
||||
common.setSelected(block);
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,14 @@ import {IPaster} from '../interfaces/i_paster.js';
|
||||
import {ICopyData} from '../interfaces/i_copyable.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {WorkspaceCommentSvg} from '../workspace_comment_svg.js';
|
||||
import * as registry from './registry.js';
|
||||
import * as commentSerialiation from '../serialization/workspace_comments.js';
|
||||
import * as eventUtils from '../events/utils.js';
|
||||
import * as common from '../common.js';
|
||||
import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js';
|
||||
|
||||
export class WorkspaceCommentPaster
|
||||
implements IPaster<WorkspaceCommentCopyData, WorkspaceCommentSvg>
|
||||
implements IPaster<WorkspaceCommentCopyData, RenderedWorkspaceComment>
|
||||
{
|
||||
static TYPE = 'workspace-comment';
|
||||
|
||||
@@ -20,26 +23,72 @@ export class WorkspaceCommentPaster
|
||||
copyData: WorkspaceCommentCopyData,
|
||||
workspace: WorkspaceSvg,
|
||||
coordinate?: Coordinate,
|
||||
): WorkspaceCommentSvg {
|
||||
): RenderedWorkspaceComment | null {
|
||||
const state = copyData.commentState;
|
||||
|
||||
if (coordinate) {
|
||||
state.setAttribute('x', `${coordinate.x}`);
|
||||
state.setAttribute('y', `${coordinate.y}`);
|
||||
} else {
|
||||
const x = parseInt(state.getAttribute('x') ?? '0') + 50;
|
||||
const y = parseInt(state.getAttribute('y') ?? '0') + 50;
|
||||
state.setAttribute('x', `${x}`);
|
||||
state.setAttribute('y', `${y}`);
|
||||
state['x'] = coordinate.x;
|
||||
state['y'] = coordinate.y;
|
||||
}
|
||||
return WorkspaceCommentSvg.fromXmlRendered(
|
||||
copyData.commentState,
|
||||
workspace,
|
||||
);
|
||||
|
||||
eventUtils.disable();
|
||||
let comment;
|
||||
try {
|
||||
comment = commentSerialiation.append(
|
||||
state,
|
||||
workspace,
|
||||
) as RenderedWorkspaceComment;
|
||||
moveCommentToNotConflict(comment);
|
||||
} finally {
|
||||
eventUtils.enable();
|
||||
}
|
||||
|
||||
if (!comment) return null;
|
||||
|
||||
if (eventUtils.isEnabled()) {
|
||||
eventUtils.fire(new (eventUtils.get(eventUtils.COMMENT_CREATE))(comment));
|
||||
}
|
||||
common.setSelected(comment);
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
|
||||
function moveCommentToNotConflict(comment: RenderedWorkspaceComment) {
|
||||
const workspace = comment.workspace;
|
||||
const translateDistance = 30;
|
||||
const coord = comment.getRelativeToSurfaceXY();
|
||||
const offset = new Coordinate(0, 0);
|
||||
// getRelativeToSurfaceXY is really expensive, so we want to cache this.
|
||||
const otherCoords = workspace
|
||||
.getTopComments(false)
|
||||
.filter((otherComment) => otherComment.id !== comment.id)
|
||||
.map((c) => c.getRelativeToSurfaceXY());
|
||||
|
||||
while (
|
||||
commentOverlapsOtherExactly(Coordinate.sum(coord, offset), otherCoords)
|
||||
) {
|
||||
offset.translate(
|
||||
workspace.RTL ? -translateDistance : translateDistance,
|
||||
translateDistance,
|
||||
);
|
||||
}
|
||||
|
||||
comment.moveTo(Coordinate.sum(coord, offset));
|
||||
}
|
||||
|
||||
function commentOverlapsOtherExactly(
|
||||
coord: Coordinate,
|
||||
otherCoords: Coordinate[],
|
||||
): boolean {
|
||||
return otherCoords.some(
|
||||
(otherCoord) =>
|
||||
Math.abs(otherCoord.x - coord.x) <= 1 &&
|
||||
Math.abs(otherCoord.y - coord.y) <= 1,
|
||||
);
|
||||
}
|
||||
|
||||
export interface WorkspaceCommentCopyData extends ICopyData {
|
||||
commentState: Element;
|
||||
commentState: commentSerialiation.State;
|
||||
}
|
||||
|
||||
registry.register(WorkspaceCommentPaster.TYPE, new WorkspaceCommentPaster());
|
||||
|
||||
9
core/comments.ts
Normal file
9
core/comments.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export {CommentView} from './comments/comment_view.js';
|
||||
export {WorkspaceComment} from './comments/workspace_comment.js';
|
||||
export {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
|
||||
872
core/comments/comment_view.ts
Normal file
872
core/comments/comment_view.ts
Normal file
@@ -0,0 +1,872 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {IRenderedElement} from '../interfaces/i_rendered_element.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import * as dom from '../utils/dom.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import * as layers from '../layers.js';
|
||||
import * as css from '../css.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import * as browserEvents from '../browser_events.js';
|
||||
import * as touch from '../touch.js';
|
||||
|
||||
export class CommentView implements IRenderedElement {
|
||||
/** The root group element of the comment view. */
|
||||
private svgRoot: SVGGElement;
|
||||
|
||||
/**
|
||||
* The svg rect element that we use to create a hightlight around the comment.
|
||||
*/
|
||||
private highlightRect: SVGRectElement;
|
||||
|
||||
/** The group containing all of the top bar elements. */
|
||||
private topBarGroup: SVGGElement;
|
||||
|
||||
/** The rect background for the top bar. */
|
||||
private topBarBackground: SVGRectElement;
|
||||
|
||||
/** The delete icon that goes in the top bar. */
|
||||
private deleteIcon: SVGImageElement;
|
||||
|
||||
/** The foldout icon that goes in the top bar. */
|
||||
private foldoutIcon: SVGImageElement;
|
||||
|
||||
/** The text element that goes in the top bar. */
|
||||
private textPreview: SVGTextElement;
|
||||
|
||||
/** The actual text node in the text preview. */
|
||||
private textPreviewNode: Text;
|
||||
|
||||
/** The resize handle element. */
|
||||
private resizeHandle: SVGImageElement;
|
||||
|
||||
/** The foreignObject containing the HTML text area. */
|
||||
private foreignObject: SVGForeignObjectElement;
|
||||
|
||||
/** The text area where the user can type. */
|
||||
private textArea: HTMLTextAreaElement;
|
||||
|
||||
/** The current size of the comment in workspace units. */
|
||||
private size: Size = new Size(120, 100);
|
||||
|
||||
/** Whether the comment is collapsed or not. */
|
||||
private collapsed: boolean = false;
|
||||
|
||||
/** Whether the comment is editable or not. */
|
||||
private editable: boolean = true;
|
||||
|
||||
/** The current location of the comment in workspace coordinates. */
|
||||
private location: Coordinate = new Coordinate(0, 0);
|
||||
|
||||
/** The current text of the comment. Updates on text area change. */
|
||||
private text: string = '';
|
||||
|
||||
/** Listeners for changes to text. */
|
||||
private textChangeListeners: Array<
|
||||
(oldText: string, newText: string) => void
|
||||
> = [];
|
||||
|
||||
/** Listeners for changes to size. */
|
||||
private sizeChangeListeners: Array<(oldSize: Size, newSize: Size) => void> =
|
||||
[];
|
||||
|
||||
/** Listeners for disposal. */
|
||||
private disposeListeners: Array<() => void> = [];
|
||||
|
||||
/** Listeners for collapsing. */
|
||||
private collapseChangeListeners: Array<(newCollapse: boolean) => void> = [];
|
||||
|
||||
/**
|
||||
* Event data for the pointer up event on the resize handle. Used to
|
||||
* unregister the listener.
|
||||
*/
|
||||
private resizePointerUpListener: browserEvents.Data | null = null;
|
||||
|
||||
/**
|
||||
* Event data for the pointer move event on the resize handle. Used to
|
||||
* unregister the listener.
|
||||
*/
|
||||
private resizePointerMoveListener: browserEvents.Data | null = null;
|
||||
|
||||
/** Whether this comment view is currently being disposed or not. */
|
||||
private disposing = false;
|
||||
|
||||
/** Whether this comment view has been disposed or not. */
|
||||
private disposed = false;
|
||||
|
||||
constructor(private readonly workspace: WorkspaceSvg) {
|
||||
this.svgRoot = dom.createSvgElement(Svg.G, {
|
||||
'class': 'blocklyComment blocklyEditable',
|
||||
});
|
||||
|
||||
this.highlightRect = this.createHighlightRect(this.svgRoot);
|
||||
|
||||
({
|
||||
topBarGroup: this.topBarGroup,
|
||||
topBarBackground: this.topBarBackground,
|
||||
deleteIcon: this.deleteIcon,
|
||||
foldoutIcon: this.foldoutIcon,
|
||||
textPreview: this.textPreview,
|
||||
textPreviewNode: this.textPreviewNode,
|
||||
} = this.createTopBar(this.svgRoot, workspace));
|
||||
|
||||
({foreignObject: this.foreignObject, textArea: this.textArea} =
|
||||
this.createTextArea(this.svgRoot));
|
||||
|
||||
this.resizeHandle = this.createResizeHandle(this.svgRoot, workspace);
|
||||
|
||||
// TODO: Remove this comment before merging.
|
||||
// I think we want comments to exist on the same layer as blocks.
|
||||
workspace.getLayerManager()?.append(this, layers.BLOCK);
|
||||
|
||||
// Set size to the default size.
|
||||
this.setSize(this.size);
|
||||
|
||||
// Set default transform (including inverted scale for RTL).
|
||||
this.moveTo(new Coordinate(0, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the rect we use for highlighting the comment when it's selected.
|
||||
*/
|
||||
private createHighlightRect(svgRoot: SVGGElement): SVGRectElement {
|
||||
return dom.createSvgElement(
|
||||
Svg.RECT,
|
||||
{'class': 'blocklyCommentHighlight'},
|
||||
svgRoot,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the top bar and the elements visually within it.
|
||||
* Registers event listeners.
|
||||
*/
|
||||
private createTopBar(
|
||||
svgRoot: SVGGElement,
|
||||
workspace: WorkspaceSvg,
|
||||
): {
|
||||
topBarGroup: SVGGElement;
|
||||
topBarBackground: SVGRectElement;
|
||||
deleteIcon: SVGImageElement;
|
||||
foldoutIcon: SVGImageElement;
|
||||
textPreview: SVGTextElement;
|
||||
textPreviewNode: Text;
|
||||
} {
|
||||
const topBarGroup = dom.createSvgElement(
|
||||
Svg.G,
|
||||
{
|
||||
'class': 'blocklyCommentTopbar',
|
||||
},
|
||||
svgRoot,
|
||||
);
|
||||
const topBarBackground = dom.createSvgElement(
|
||||
Svg.RECT,
|
||||
{
|
||||
'class': 'blocklyCommentTopbarBackground',
|
||||
},
|
||||
topBarGroup,
|
||||
);
|
||||
// TODO: Before merging, does this mean to override an individual image,
|
||||
// folks need to replace the whole media folder?
|
||||
const deleteIcon = dom.createSvgElement(
|
||||
Svg.IMAGE,
|
||||
{
|
||||
'class': 'blocklyDeleteIcon',
|
||||
'href': `${workspace.options.pathToMedia}delete-icon.svg`,
|
||||
},
|
||||
topBarGroup,
|
||||
);
|
||||
const foldoutIcon = dom.createSvgElement(
|
||||
Svg.IMAGE,
|
||||
{
|
||||
'class': 'blocklyFoldoutIcon',
|
||||
'href': `${workspace.options.pathToMedia}foldout-icon.svg`,
|
||||
},
|
||||
topBarGroup,
|
||||
);
|
||||
const textPreview = dom.createSvgElement(
|
||||
Svg.TEXT,
|
||||
{
|
||||
'class': 'blocklyCommentPreview blocklyCommentText blocklyText',
|
||||
},
|
||||
topBarGroup,
|
||||
);
|
||||
const textPreviewNode = document.createTextNode('');
|
||||
textPreview.appendChild(textPreviewNode);
|
||||
|
||||
// TODO(toychest): Triggering this on pointerdown means that we can't start
|
||||
// drags on the foldout icon. We need to open up the gesture system
|
||||
// to fix this.
|
||||
browserEvents.conditionalBind(
|
||||
foldoutIcon,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.onFoldoutDown,
|
||||
);
|
||||
browserEvents.conditionalBind(
|
||||
deleteIcon,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.onDeleteDown,
|
||||
);
|
||||
|
||||
return {
|
||||
topBarGroup,
|
||||
topBarBackground,
|
||||
deleteIcon,
|
||||
foldoutIcon,
|
||||
textPreview,
|
||||
textPreviewNode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the text area where users can type. Registers event listeners.
|
||||
*/
|
||||
private createTextArea(svgRoot: SVGGElement): {
|
||||
foreignObject: SVGForeignObjectElement;
|
||||
textArea: HTMLTextAreaElement;
|
||||
} {
|
||||
const foreignObject = dom.createSvgElement(
|
||||
Svg.FOREIGNOBJECT,
|
||||
{
|
||||
'class': 'blocklyCommentForeignObject',
|
||||
},
|
||||
svgRoot,
|
||||
);
|
||||
const body = document.createElementNS(dom.HTML_NS, 'body');
|
||||
body.setAttribute('xmlns', dom.HTML_NS);
|
||||
body.className = 'blocklyMinimalBody';
|
||||
const textArea = document.createElementNS(
|
||||
dom.HTML_NS,
|
||||
'textarea',
|
||||
) as HTMLTextAreaElement;
|
||||
dom.addClass(textArea, 'blocklyCommentText');
|
||||
dom.addClass(textArea, 'blocklyTextarea');
|
||||
dom.addClass(textArea, 'blocklyText');
|
||||
body.appendChild(textArea);
|
||||
foreignObject.appendChild(body);
|
||||
|
||||
browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange);
|
||||
|
||||
return {foreignObject, textArea};
|
||||
}
|
||||
|
||||
/** Creates the DOM elements for the comment resize handle. */
|
||||
private createResizeHandle(
|
||||
svgRoot: SVGGElement,
|
||||
workspace: WorkspaceSvg,
|
||||
): SVGImageElement {
|
||||
const resizeHandle = dom.createSvgElement(
|
||||
Svg.IMAGE,
|
||||
{
|
||||
'class': 'blocklyResizeHandle',
|
||||
'href': `${workspace.options.pathToMedia}resize-handle.svg`,
|
||||
},
|
||||
svgRoot,
|
||||
);
|
||||
|
||||
browserEvents.conditionalBind(
|
||||
resizeHandle,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.onResizePointerDown,
|
||||
);
|
||||
|
||||
return resizeHandle;
|
||||
}
|
||||
|
||||
/** Returns the root SVG group element of the comment view. */
|
||||
getSvgRoot(): SVGGElement {
|
||||
return this.svgRoot;
|
||||
}
|
||||
|
||||
/** Returns the current size of the comment in workspace units. */
|
||||
getSize(): Size {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of the comment in workspace units, and updates the view
|
||||
* elements to reflect the new size.
|
||||
*/
|
||||
setSize(size: Size) {
|
||||
const topBarSize = this.topBarBackground.getBBox();
|
||||
const deleteSize = this.deleteIcon.getBBox();
|
||||
const foldoutSize = this.foldoutIcon.getBBox();
|
||||
const textPreviewSize = this.textPreview.getBBox();
|
||||
const resizeSize = this.resizeHandle.getBBox();
|
||||
|
||||
size = Size.max(
|
||||
size,
|
||||
this.calcMinSize(topBarSize, foldoutSize, deleteSize),
|
||||
);
|
||||
const oldSize = this.size;
|
||||
this.size = size;
|
||||
|
||||
this.svgRoot.setAttribute('height', `${size.height}`);
|
||||
this.svgRoot.setAttribute('width', `${size.width}`);
|
||||
|
||||
this.updateHighlightRect(size);
|
||||
this.updateTopBarSize(size);
|
||||
this.updateTextAreaSize(size, topBarSize);
|
||||
this.updateDeleteIconPosition(size, topBarSize, deleteSize);
|
||||
this.updateFoldoutIconPosition(topBarSize, foldoutSize);
|
||||
this.updateTextPreviewSize(
|
||||
size,
|
||||
topBarSize,
|
||||
textPreviewSize,
|
||||
deleteSize,
|
||||
resizeSize,
|
||||
);
|
||||
this.updateResizeHandlePosition(size, resizeSize);
|
||||
|
||||
this.onSizeChange(oldSize, this.size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the minimum size for the uncollapsed comment based on text
|
||||
* size and visible icons.
|
||||
*
|
||||
* The minimum width is based on the width of the truncated preview text.
|
||||
*
|
||||
* The minimum height is based on the height of the top bar.
|
||||
*/
|
||||
private calcMinSize(
|
||||
topBarSize: Size,
|
||||
foldoutSize: Size,
|
||||
deleteSize: Size,
|
||||
): Size {
|
||||
this.updateTextPreview(this.textArea.value ?? '');
|
||||
const textPreviewWidth = dom.getTextWidth(this.textPreview);
|
||||
|
||||
const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize);
|
||||
const deleteMargin = this.calcDeleteMargin(topBarSize, deleteSize);
|
||||
|
||||
let width = textPreviewWidth;
|
||||
if (this.foldoutIcon.checkVisibility()) {
|
||||
width += foldoutSize.width + foldoutMargin * 2;
|
||||
} else if (textPreviewWidth) {
|
||||
width += 4; // Arbitrary margin before text.
|
||||
}
|
||||
if (this.deleteIcon.checkVisibility()) {
|
||||
width += deleteSize.width + deleteMargin * 2;
|
||||
} else if (textPreviewWidth) {
|
||||
width += 4; // Arbitrary margin after text.
|
||||
}
|
||||
|
||||
// Arbitrary additional height.
|
||||
const height = topBarSize.height + 20;
|
||||
|
||||
return new Size(width, height);
|
||||
}
|
||||
|
||||
/** Calculates the margin that should exist around the delete icon. */
|
||||
private calcDeleteMargin(topBarSize: Size, deleteSize: Size) {
|
||||
return (topBarSize.height - deleteSize.height) / 2;
|
||||
}
|
||||
|
||||
/** Calculates the margin that should exist around the foldout icon. */
|
||||
private calcFoldoutMargin(topBarSize: Size, foldoutSize: Size) {
|
||||
return (topBarSize.height - foldoutSize.height) / 2;
|
||||
}
|
||||
|
||||
/** Updates the size of the highlight rect to reflect the new size. */
|
||||
private updateHighlightRect(size: Size) {
|
||||
this.highlightRect.setAttribute('height', `${size.height}`);
|
||||
this.highlightRect.setAttribute('width', `${size.width}`);
|
||||
if (this.workspace.RTL) {
|
||||
this.highlightRect.setAttribute('x', `${-size.width}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the size of the top bar to reflect the new size. */
|
||||
private updateTopBarSize(size: Size) {
|
||||
this.topBarBackground.setAttribute('width', `${size.width}`);
|
||||
}
|
||||
|
||||
/** Updates the size of the text area elements to reflect the new size. */
|
||||
private updateTextAreaSize(size: Size, topBarSize: Size) {
|
||||
this.foreignObject.setAttribute(
|
||||
'height',
|
||||
`${size.height - topBarSize.height}`,
|
||||
);
|
||||
this.foreignObject.setAttribute('width', `${size.width}`);
|
||||
this.foreignObject.setAttribute('y', `${topBarSize.height}`);
|
||||
if (this.workspace.RTL) {
|
||||
this.foreignObject.setAttribute('x', `${-size.width}`);
|
||||
}
|
||||
this.textArea.style.width = `${size.width}px`;
|
||||
this.textArea.style.height = `${size.height}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the position of the delete icon elements to reflect the new size.
|
||||
*/
|
||||
private updateDeleteIconPosition(
|
||||
size: Size,
|
||||
topBarSize: Size,
|
||||
deleteSize: Size,
|
||||
) {
|
||||
const deleteMargin = this.calcDeleteMargin(topBarSize, deleteSize);
|
||||
this.deleteIcon.setAttribute('y', `${deleteMargin}`);
|
||||
this.deleteIcon.setAttribute(
|
||||
'x',
|
||||
`${size.width - deleteSize.width - deleteMargin}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the position of the foldout icon elements to reflect the new size.
|
||||
*/
|
||||
private updateFoldoutIconPosition(topBarSize: Size, foldoutSize: Size) {
|
||||
const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize);
|
||||
this.foldoutIcon.setAttribute('y', `${foldoutMargin}`);
|
||||
this.foldoutIcon.setAttribute('x', `${foldoutMargin}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the size and position of the text preview elements to reflect the new size.
|
||||
*/
|
||||
private updateTextPreviewSize(
|
||||
size: Size,
|
||||
topBarSize: Size,
|
||||
textPreviewSize: Size,
|
||||
deleteSize: Size,
|
||||
foldoutSize: Size,
|
||||
) {
|
||||
const textPreviewMargin = (topBarSize.height - textPreviewSize.height) / 2;
|
||||
const deleteMargin = this.calcDeleteMargin(topBarSize, deleteSize);
|
||||
const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize);
|
||||
|
||||
const textPreviewWidth =
|
||||
size.width -
|
||||
foldoutSize.width -
|
||||
foldoutMargin * 2 -
|
||||
deleteSize.width -
|
||||
deleteMargin * 2;
|
||||
this.textPreview.setAttribute(
|
||||
'x',
|
||||
`${
|
||||
foldoutSize.width + foldoutMargin * 2 * (this.workspace.RTL ? -1 : 1)
|
||||
}`,
|
||||
);
|
||||
this.textPreview.setAttribute(
|
||||
'y',
|
||||
`${textPreviewMargin + textPreviewSize.height / 2}`,
|
||||
);
|
||||
this.textPreview.setAttribute('width', `${textPreviewWidth}`);
|
||||
}
|
||||
|
||||
/** Updates the position of the resize handle to reflect the new size. */
|
||||
private updateResizeHandlePosition(size: Size, resizeSize: Size) {
|
||||
this.resizeHandle.setAttribute('y', `${size.height - resizeSize.height}`);
|
||||
this.resizeHandle.setAttribute('x', `${size.width - resizeSize.width}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers listeners when the size of the comment changes, either
|
||||
* progrmatically or manually by the user.
|
||||
*/
|
||||
private onSizeChange(oldSize: Size, newSize: Size) {
|
||||
// Loop through listeners backwards in case they remove themselves.
|
||||
for (let i = this.sizeChangeListeners.length - 1; i >= 0; i--) {
|
||||
this.sizeChangeListeners[i](oldSize, newSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback that listens for size changes.
|
||||
*
|
||||
* @param listener Receives callbacks when the size of the comment changes.
|
||||
* The new and old size are in workspace units.
|
||||
*/
|
||||
addSizeChangeListener(listener: (oldSize: Size, newSize: Size) => void) {
|
||||
this.sizeChangeListeners.push(listener);
|
||||
}
|
||||
|
||||
/** Removes the given listener from the list of size change listeners. */
|
||||
removeSizeChangeListener(listener: () => void) {
|
||||
this.sizeChangeListeners.splice(
|
||||
this.sizeChangeListeners.indexOf(listener),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles starting an interaction with the resize handle to resize the
|
||||
* comment.
|
||||
*/
|
||||
private onResizePointerDown(e: PointerEvent) {
|
||||
if (!this.isEditable()) return;
|
||||
|
||||
this.bringToFront();
|
||||
if (browserEvents.isRightButton(e)) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(#7926): Move this into a utils file.
|
||||
this.workspace.startDrag(
|
||||
e,
|
||||
new Coordinate(
|
||||
this.workspace.RTL ? -this.getSize().width : this.getSize().width,
|
||||
this.getSize().height,
|
||||
),
|
||||
);
|
||||
|
||||
this.resizePointerUpListener = browserEvents.conditionalBind(
|
||||
document,
|
||||
'pointerup',
|
||||
this,
|
||||
this.onResizePointerUp,
|
||||
);
|
||||
this.resizePointerMoveListener = browserEvents.conditionalBind(
|
||||
document,
|
||||
'pointermove',
|
||||
this,
|
||||
this.onResizePointerMove,
|
||||
);
|
||||
|
||||
this.workspace.hideChaff();
|
||||
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/** Ends an interaction with the resize handle. */
|
||||
private onResizePointerUp(_e: PointerEvent) {
|
||||
touch.clearTouchIdentifier();
|
||||
if (this.resizePointerUpListener) {
|
||||
browserEvents.unbind(this.resizePointerUpListener);
|
||||
this.resizePointerUpListener = null;
|
||||
}
|
||||
if (this.resizePointerMoveListener) {
|
||||
browserEvents.unbind(this.resizePointerMoveListener);
|
||||
this.resizePointerMoveListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Resizes the comment in response to a drag on the resize handle. */
|
||||
private onResizePointerMove(e: PointerEvent) {
|
||||
// TODO(#7926): Move this into a utils file.
|
||||
const delta = this.workspace.moveDrag(e);
|
||||
this.setSize(new Size(this.workspace.RTL ? -delta.x : delta.x, delta.y));
|
||||
}
|
||||
|
||||
/** Returns true if the comment is currently collapsed. */
|
||||
isCollapsed(): boolean {
|
||||
return this.collapsed;
|
||||
}
|
||||
|
||||
/** Sets whether the comment is currently collapsed or not. */
|
||||
setCollapsed(collapsed: boolean) {
|
||||
this.collapsed = collapsed;
|
||||
if (collapsed) {
|
||||
dom.addClass(this.svgRoot, 'blocklyCollapsed');
|
||||
} else {
|
||||
dom.removeClass(this.svgRoot, 'blocklyCollapsed');
|
||||
}
|
||||
// Repositions resize handle and such.
|
||||
this.setSize(this.size);
|
||||
this.onCollapse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers listeners when the collapsed-ness of the comment changes, either
|
||||
* progrmatically or manually by the user.
|
||||
*/
|
||||
private onCollapse() {
|
||||
// Loop through listeners backwards in case they remove themselves.
|
||||
for (let i = this.collapseChangeListeners.length - 1; i >= 0; i--) {
|
||||
this.collapseChangeListeners[i](this.collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
/** Registers a callback that listens for collapsed-ness changes. */
|
||||
addOnCollapseListener(listener: (newCollapse: boolean) => void) {
|
||||
this.collapseChangeListeners.push(listener);
|
||||
}
|
||||
|
||||
/** Removes the given listener from the list of on collapse listeners. */
|
||||
removeOnCollapseListener(listener: () => void) {
|
||||
this.collapseChangeListeners.splice(
|
||||
this.collapseChangeListeners.indexOf(listener),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the collapsedness of the block when we receive a pointer down
|
||||
* event on the foldout icon.
|
||||
*/
|
||||
private onFoldoutDown(e: PointerEvent) {
|
||||
this.bringToFront();
|
||||
if (browserEvents.isRightButton(e)) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setCollapsed(!this.collapsed);
|
||||
|
||||
this.workspace.hideChaff();
|
||||
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/** Returns true if the comment is currently editable. */
|
||||
isEditable(): boolean {
|
||||
return this.editable;
|
||||
}
|
||||
|
||||
/** Sets the editability of the comment. */
|
||||
setEditable(editable: boolean) {
|
||||
this.editable = editable;
|
||||
if (this.editable) {
|
||||
dom.addClass(this.svgRoot, 'blocklyEditable');
|
||||
dom.removeClass(this.svgRoot, 'blocklyReadonly');
|
||||
this.textArea.removeAttribute('readonly');
|
||||
} else {
|
||||
dom.removeClass(this.svgRoot, 'blocklyEditable');
|
||||
dom.addClass(this.svgRoot, 'blocklyReadonly');
|
||||
this.textArea.setAttribute('readonly', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the current location of the comment in workspace coordinates. */
|
||||
getRelativeToSurfaceXY(): Coordinate {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the comment view to the given location.
|
||||
*
|
||||
* @param location The location to move to in workspace coordinates.
|
||||
*/
|
||||
moveTo(location: Coordinate) {
|
||||
this.location = location;
|
||||
this.svgRoot.setAttribute(
|
||||
'transform',
|
||||
`translate(${location.x}, ${location.y})`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Retursn the current text of the comment. */
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
/** Sets the current text of the comment. */
|
||||
setText(text: string) {
|
||||
this.textArea.value = text;
|
||||
this.onTextChange();
|
||||
}
|
||||
|
||||
/** Registers a callback that listens for text changes. */
|
||||
addTextChangeListener(listener: (oldText: string, newText: string) => void) {
|
||||
this.textChangeListeners.push(listener);
|
||||
}
|
||||
|
||||
/** Removes the given listener from the list of text change listeners. */
|
||||
removeTextChangeListener(listener: () => void) {
|
||||
this.textChangeListeners.splice(
|
||||
this.textChangeListeners.indexOf(listener),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers listeners when the text of the comment changes, either
|
||||
* progrmatically or manually by the user.
|
||||
*/
|
||||
private onTextChange() {
|
||||
const oldText = this.text;
|
||||
this.text = this.textArea.value;
|
||||
this.updateTextPreview(this.text);
|
||||
// Update size in case our minimum size increased.
|
||||
this.setSize(this.size);
|
||||
// Loop through listeners backwards in case they remove themselves.
|
||||
for (let i = this.textChangeListeners.length - 1; i >= 0; i--) {
|
||||
this.textChangeListeners[i](oldText, this.text);
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the preview text element to reflect the given text. */
|
||||
private updateTextPreview(text: string) {
|
||||
this.textPreviewNode.textContent = this.truncateText(text);
|
||||
}
|
||||
|
||||
/** Truncates the text to fit within the top view. */
|
||||
private truncateText(text: string): string {
|
||||
return text.length >= 12 ? `${text.substring(0, 9)}...` : text;
|
||||
}
|
||||
|
||||
/** Brings the workspace comment to the front of its layer. */
|
||||
private bringToFront() {
|
||||
const parent = this.svgRoot.parentNode;
|
||||
const childNodes = parent!.childNodes;
|
||||
// Avoid moving the comment if it's already at the bottom.
|
||||
if (childNodes[childNodes.length - 1] !== this.svgRoot) {
|
||||
parent!.appendChild(this.svgRoot);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles disposing of the comment when we get a pointer down event on the
|
||||
* delete icon.
|
||||
*/
|
||||
private onDeleteDown(e: PointerEvent) {
|
||||
if (browserEvents.isRightButton(e)) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispose();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/** Disposes of this comment view. */
|
||||
dispose() {
|
||||
this.disposing = true;
|
||||
dom.removeNode(this.svgRoot);
|
||||
// Loop through listeners backwards in case they remove themselves.
|
||||
for (let i = this.disposeListeners.length - 1; i >= 0; i--) {
|
||||
this.disposeListeners[i]();
|
||||
}
|
||||
this.disposed = true;
|
||||
}
|
||||
|
||||
/** Returns whether this comment view has been disposed or not. */
|
||||
isDisposed(): boolean {
|
||||
return this.disposed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this comment view is currently being disposed or has
|
||||
* already been disposed.
|
||||
*/
|
||||
isDeadOrDying(): boolean {
|
||||
return this.disposing || this.disposed;
|
||||
}
|
||||
|
||||
/** Registers a callback that listens for disposal of this view. */
|
||||
addDisposeListener(listener: () => void) {
|
||||
this.disposeListeners.push(listener);
|
||||
}
|
||||
|
||||
/** Removes the given listener from the list of disposal listeners. */
|
||||
removeDisposeListener(listener: () => void) {
|
||||
this.disposeListeners.splice(this.disposeListeners.indexOf(listener), 1);
|
||||
}
|
||||
}
|
||||
|
||||
css.register(`
|
||||
.injectionDiv {
|
||||
--commentFillColour: #FFFCC7;
|
||||
--commentBorderColour: #F2E49B;
|
||||
}
|
||||
|
||||
.blocklyComment .blocklyTextarea {
|
||||
background-color: var(--commentFillColour);
|
||||
border: 1px solid var(--commentBorderColour);
|
||||
outline: 0;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.blocklyReadonly.blocklyComment .blocklyTextarea {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.blocklyDeleteIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.blocklyFoldoutIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transform-origin: 12px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.blocklyResizeHandle {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
cursor: se-resize;
|
||||
}
|
||||
.blocklyReadonly.blocklyComment .blocklyResizeHandle {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.blocklyCommentTopbarBackground {
|
||||
fill: var(--commentBorderColour);
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.blocklyComment .blocklyCommentPreview.blocklyText {
|
||||
fill: #000;
|
||||
dominant-baseline: middle;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.blocklyCollapsed.blocklyComment .blocklyCommentPreview {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.blocklyCollapsed.blocklyComment .blocklyCommentForeignObject,
|
||||
.blocklyCollapsed.blocklyComment .blocklyResizeHandle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blocklyCollapsed.blocklyComment .blocklyFoldoutIcon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.blocklyRTL .blocklyCommentTopbar {
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
.blocklyRTL .blocklyCommentForeignObject {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.blocklyRTL .blocklyCommentPreview {
|
||||
/* Revert the scale and control RTL using direction instead. */
|
||||
transform: scale(-1, 1);
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.blocklyRTL .blocklyResizeHandle {
|
||||
transform: scale(-1, 1);
|
||||
cursor: sw-resize;
|
||||
}
|
||||
|
||||
.blocklyCommentHighlight {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.blocklySelected .blocklyCommentHighlight {
|
||||
stroke: #fc3;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
.blocklyCollapsed.blocklySelected .blocklyCommentHighlight {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.blocklyCollapsed.blocklySelected .blocklyCommentTopbarBackground {
|
||||
stroke: #fc3;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
`);
|
||||
264
core/comments/rendered_workspace_comment.ts
Normal file
264
core/comments/rendered_workspace_comment.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {WorkspaceComment} from './workspace_comment.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {CommentView} from './comment_view.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import {Rect} from '../utils/rect.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import {IBoundedElement} from '../interfaces/i_bounded_element.js';
|
||||
import {IRenderedElement} from '../interfaces/i_rendered_element.js';
|
||||
import * as dom from '../utils/dom.js';
|
||||
import {IDraggable} from '../interfaces/i_draggable.js';
|
||||
import {CommentDragStrategy} from '../dragging/comment_drag_strategy.js';
|
||||
import * as browserEvents from '../browser_events.js';
|
||||
import * as common from '../common.js';
|
||||
import {ISelectable} from '../interfaces/i_selectable.js';
|
||||
import {IDeletable} from '../interfaces/i_deletable.js';
|
||||
import {ICopyable} from '../interfaces/i_copyable.js';
|
||||
import * as commentSerialization from '../serialization/workspace_comments.js';
|
||||
import {
|
||||
WorkspaceCommentPaster,
|
||||
WorkspaceCommentCopyData,
|
||||
} from '../clipboard/workspace_comment_paster.js';
|
||||
import {IContextMenu} from '../interfaces/i_contextmenu.js';
|
||||
import * as contextMenu from '../contextmenu.js';
|
||||
import {ContextMenuRegistry} from '../contextmenu_registry.js';
|
||||
|
||||
export class RenderedWorkspaceComment
|
||||
extends WorkspaceComment
|
||||
implements
|
||||
IBoundedElement,
|
||||
IRenderedElement,
|
||||
IDraggable,
|
||||
ISelectable,
|
||||
IDeletable,
|
||||
ICopyable<WorkspaceCommentCopyData>,
|
||||
IContextMenu
|
||||
{
|
||||
/** The class encompassing the svg elements making up the workspace comment. */
|
||||
private view: CommentView;
|
||||
|
||||
public readonly workspace: WorkspaceSvg;
|
||||
|
||||
private dragStrategy = new CommentDragStrategy(this);
|
||||
|
||||
/** Constructs the workspace comment, including the view. */
|
||||
constructor(workspace: WorkspaceSvg, id?: string) {
|
||||
super(workspace, id);
|
||||
|
||||
this.workspace = workspace;
|
||||
|
||||
this.view = new CommentView(workspace);
|
||||
// Set the size to the default size as defined in the superclass.
|
||||
this.view.setSize(this.getSize());
|
||||
this.view.setEditable(this.isEditable());
|
||||
|
||||
this.addModelUpdateBindings();
|
||||
|
||||
browserEvents.conditionalBind(
|
||||
this.view.getSvgRoot(),
|
||||
'pointerdown',
|
||||
this,
|
||||
this.startGesture,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds listeners to the view that updates the model (i.e. the superclass)
|
||||
* when changes are made to the view.
|
||||
*/
|
||||
private addModelUpdateBindings() {
|
||||
this.view.addTextChangeListener(
|
||||
(_, newText: string) => void super.setText(newText),
|
||||
);
|
||||
this.view.addSizeChangeListener(
|
||||
(_, newSize: Size) => void super.setSize(newSize),
|
||||
);
|
||||
this.view.addOnCollapseListener(
|
||||
() => void super.setCollapsed(this.view.isCollapsed()),
|
||||
);
|
||||
this.view.addDisposeListener(() => {
|
||||
if (!this.isDeadOrDying()) this.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
/** Sets the text of the comment. */
|
||||
override setText(text: string): void {
|
||||
// setText will trigger the change listener that updates
|
||||
// the model aka superclass.
|
||||
this.view.setText(text);
|
||||
}
|
||||
|
||||
/** Sets the size of the comment. */
|
||||
override setSize(size: Size) {
|
||||
// setSize will trigger the change listener that updates
|
||||
// the model aka superclass.
|
||||
this.view.setSize(size);
|
||||
}
|
||||
|
||||
/** Sets whether the comment is collapsed or not. */
|
||||
override setCollapsed(collapsed: boolean) {
|
||||
// setCollapsed will trigger the change listener that updates
|
||||
// the model aka superclass.
|
||||
this.view.setCollapsed(collapsed);
|
||||
}
|
||||
|
||||
/** Sets whether the comment is editable or not. */
|
||||
override setEditable(editable: boolean): void {
|
||||
super.setEditable(editable);
|
||||
// Use isEditable rather than isOwnEditable to account for workspace state.
|
||||
this.view.setEditable(this.isEditable());
|
||||
}
|
||||
|
||||
/** Returns the root SVG element of this comment. */
|
||||
getSvgRoot(): SVGElement {
|
||||
return this.view.getSvgRoot();
|
||||
}
|
||||
|
||||
/** Returns the bounding rectangle of this comment in workspace coordinates. */
|
||||
getBoundingRectangle(): Rect {
|
||||
const loc = this.getRelativeToSurfaceXY();
|
||||
const size = this.getSize();
|
||||
return new Rect(loc.y, loc.y + size.height, loc.x, loc.x + size.width);
|
||||
}
|
||||
|
||||
/** Move the comment by the given amounts in workspace coordinates. */
|
||||
moveBy(dx: number, dy: number, reason?: string[] | undefined): void {
|
||||
const loc = this.getRelativeToSurfaceXY();
|
||||
const newLoc = new Coordinate(loc.x + dx, loc.y + dy);
|
||||
this.moveTo(newLoc, reason);
|
||||
}
|
||||
|
||||
/** Moves the comment to the given location in workspace coordinates. */
|
||||
override moveTo(location: Coordinate, reason?: string[] | undefined): void {
|
||||
super.moveTo(location, reason);
|
||||
this.view.moveTo(location);
|
||||
this.snapToGrid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the comment during a drag. Doesn't fire move events.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
moveDuringDrag(location: Coordinate): void {
|
||||
this.location = location;
|
||||
this.view.moveTo(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the dragging CSS class to this comment.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
setDragging(dragging: boolean): void {
|
||||
if (dragging) {
|
||||
dom.addClass(this.getSvgRoot(), 'blocklyDragging');
|
||||
} else {
|
||||
dom.removeClass(this.getSvgRoot(), 'blocklyDragging');
|
||||
}
|
||||
}
|
||||
|
||||
/** Disposes of the view. */
|
||||
override dispose() {
|
||||
this.disposing = true;
|
||||
if (!this.view.isDeadOrDying()) this.view.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a gesture because we detected a pointer down on the comment
|
||||
* (that wasn't otherwise gobbled up, e.g. by resizing).
|
||||
*/
|
||||
private startGesture(e: PointerEvent) {
|
||||
const gesture = this.workspace.getGesture(e);
|
||||
if (gesture) {
|
||||
gesture.handleCommentStart(e, this);
|
||||
common.setSelected(this);
|
||||
}
|
||||
}
|
||||
|
||||
/** Visually indicates that this comment would be deleted if dropped. */
|
||||
setDeleteStyle(wouldDelete: boolean): void {
|
||||
if (wouldDelete) {
|
||||
dom.addClass(this.getSvgRoot(), 'blocklyDraggingDelete');
|
||||
} else {
|
||||
dom.removeClass(this.getSvgRoot(), 'blocklyDraggingDelete');
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns whether this comment is movable or not. */
|
||||
isMovable(): boolean {
|
||||
return this.dragStrategy.isMovable();
|
||||
}
|
||||
|
||||
/** Starts a drag on the comment. */
|
||||
startDrag(): void {
|
||||
this.dragStrategy.startDrag();
|
||||
}
|
||||
|
||||
/** Drags the comment to the given location. */
|
||||
drag(newLoc: Coordinate): void {
|
||||
this.dragStrategy.drag(newLoc);
|
||||
}
|
||||
|
||||
/** Ends the drag on the comment. */
|
||||
endDrag(): void {
|
||||
this.snapToGrid();
|
||||
this.dragStrategy.endDrag();
|
||||
}
|
||||
|
||||
/** Moves the comment back to where it was at the start of a drag. */
|
||||
revertDrag(): void {
|
||||
this.dragStrategy.revertDrag();
|
||||
}
|
||||
|
||||
/** Visually highlights the comment. */
|
||||
select(): void {
|
||||
dom.addClass(this.getSvgRoot(), 'blocklySelected');
|
||||
}
|
||||
|
||||
/** Visually unhighlights the comment. */
|
||||
unselect(): void {
|
||||
dom.removeClass(this.getSvgRoot(), 'blocklySelected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON serializable representation of this comment's state that
|
||||
* can be used for pasting.
|
||||
*/
|
||||
toCopyData(): WorkspaceCommentCopyData | null {
|
||||
return {
|
||||
paster: WorkspaceCommentPaster.TYPE,
|
||||
commentState: commentSerialization.save(this, {
|
||||
addCoordinates: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Show a context menu for this comment. */
|
||||
showContextMenu(e: PointerEvent): void {
|
||||
const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
|
||||
ContextMenuRegistry.ScopeType.COMMENT,
|
||||
{comment: this},
|
||||
);
|
||||
contextMenu.show(e, menuOptions, this.workspace.RTL);
|
||||
}
|
||||
|
||||
/** Snap this comment to the nearest grid point. */
|
||||
snapToGrid(): void {
|
||||
if (this.isDeadOrDying()) return;
|
||||
const grid = this.workspace.getGrid();
|
||||
if (!grid?.shouldSnap()) return;
|
||||
const currentXY = this.getRelativeToSurfaceXY();
|
||||
const alignedXY = grid.alignXY(currentXY);
|
||||
if (alignedXY !== currentXY) {
|
||||
this.moveTo(alignedXY, ['snap']);
|
||||
}
|
||||
}
|
||||
}
|
||||
227
core/comments/workspace_comment.ts
Normal file
227
core/comments/workspace_comment.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {Workspace} from '../workspace.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import * as idGenerator from '../utils/idgenerator.js';
|
||||
import * as eventUtils from '../events/utils.js';
|
||||
import {CommentMove} from '../events/events_comment_move.js';
|
||||
|
||||
export class WorkspaceComment {
|
||||
/** The unique identifier for this comment. */
|
||||
public readonly id: string;
|
||||
|
||||
/** The text of the comment. */
|
||||
private text = '';
|
||||
|
||||
/** The size of the comment in workspace units. */
|
||||
private size = new Size(120, 100);
|
||||
|
||||
/** Whether the comment is collapsed or not. */
|
||||
private collapsed = false;
|
||||
|
||||
/** Whether the comment is editable or not. */
|
||||
private editable = true;
|
||||
|
||||
/** Whether the comment is movable or not. */
|
||||
private movable = true;
|
||||
|
||||
/** Whether the comment is deletable or not. */
|
||||
private deletable = true;
|
||||
|
||||
/** The location of the comment in workspace coordinates. */
|
||||
protected location = new Coordinate(0, 0);
|
||||
|
||||
/** Whether this comment has been disposed or not. */
|
||||
protected disposed = false;
|
||||
|
||||
/** Whether this comment is being disposed or not. */
|
||||
protected disposing = false;
|
||||
|
||||
/**
|
||||
* Constructs the comment.
|
||||
*
|
||||
* @param workspace The workspace to construct the comment in.
|
||||
* @param id An optional ID to give to the comment. If not provided, one will
|
||||
* be generated.
|
||||
*/
|
||||
constructor(
|
||||
public readonly workspace: Workspace,
|
||||
id?: string,
|
||||
) {
|
||||
this.id = id && !workspace.getCommentById(id) ? id : idGenerator.genUid();
|
||||
|
||||
workspace.addTopComment(this);
|
||||
|
||||
this.fireCreateEvent();
|
||||
}
|
||||
|
||||
private fireCreateEvent() {
|
||||
if (eventUtils.isEnabled()) {
|
||||
eventUtils.fire(new (eventUtils.get(eventUtils.COMMENT_CREATE))(this));
|
||||
}
|
||||
}
|
||||
|
||||
private fireDeleteEvent() {
|
||||
if (eventUtils.isEnabled()) {
|
||||
eventUtils.fire(new (eventUtils.get(eventUtils.COMMENT_DELETE))(this));
|
||||
}
|
||||
}
|
||||
|
||||
/** Fires a comment change event. */
|
||||
private fireChangeEvent(oldText: string, newText: string) {
|
||||
if (eventUtils.isEnabled()) {
|
||||
eventUtils.fire(
|
||||
new (eventUtils.get(eventUtils.COMMENT_CHANGE))(this, oldText, newText),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Fires a comment collapse event. */
|
||||
private fireCollapseEvent(newCollapsed: boolean) {
|
||||
if (eventUtils.isEnabled()) {
|
||||
eventUtils.fire(
|
||||
new (eventUtils.get(eventUtils.COMMENT_COLLAPSE))(this, newCollapsed),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the text of the comment. */
|
||||
setText(text: string) {
|
||||
const oldText = this.text;
|
||||
this.text = text;
|
||||
this.fireChangeEvent(oldText, text);
|
||||
}
|
||||
|
||||
/** Returns the text of the comment. */
|
||||
getText(): string {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
/** Sets the comment's size in workspace units. */
|
||||
setSize(size: Size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
/** Returns the comment's size in workspace units. */
|
||||
getSize(): Size {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
/** Sets whether the comment is collapsed or not. */
|
||||
setCollapsed(collapsed: boolean) {
|
||||
this.collapsed = collapsed;
|
||||
this.fireCollapseEvent(collapsed);
|
||||
}
|
||||
|
||||
/** Returns whether the comment is collapsed or not. */
|
||||
isCollapsed(): boolean {
|
||||
return this.collapsed;
|
||||
}
|
||||
|
||||
/** Sets whether the comment is editable or not. */
|
||||
setEditable(editable: boolean) {
|
||||
this.editable = editable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the comment is editable or not, respecting whether the
|
||||
* workspace is read-only.
|
||||
*/
|
||||
isEditable(): boolean {
|
||||
return this.isOwnEditable() && !this.workspace.options.readOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the comment is editable or not, only examining its own
|
||||
* state and ignoring the state of the workspace.
|
||||
*/
|
||||
isOwnEditable(): boolean {
|
||||
return this.editable;
|
||||
}
|
||||
|
||||
/** Sets whether the comment is movable or not. */
|
||||
setMovable(movable: boolean) {
|
||||
this.movable = movable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the comment is movable or not, respecting whether the
|
||||
* workspace is read-only.
|
||||
*/
|
||||
isMovable() {
|
||||
return this.isOwnMovable() && !this.workspace.options.readOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the comment is movable or not, only examining its own
|
||||
* state and ignoring the state of the workspace.
|
||||
*/
|
||||
isOwnMovable() {
|
||||
return this.movable;
|
||||
}
|
||||
|
||||
/** Sets whether the comment is deletable or not. */
|
||||
setDeletable(deletable: boolean) {
|
||||
this.deletable = deletable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the comment is deletable or not, respecting whether the
|
||||
* workspace is read-only.
|
||||
*/
|
||||
isDeletable(): boolean {
|
||||
return this.isOwnDeletable() && !this.workspace.options.readOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the comment is deletable or not, only examining its own
|
||||
* state and ignoring the state of the workspace.
|
||||
*/
|
||||
isOwnDeletable(): boolean {
|
||||
return this.deletable;
|
||||
}
|
||||
|
||||
/** Moves the comment to the given location in workspace coordinates. */
|
||||
moveTo(location: Coordinate, reason?: string[] | undefined) {
|
||||
const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(
|
||||
this,
|
||||
) as CommentMove;
|
||||
if (reason) event.setReason(reason);
|
||||
|
||||
this.location = location;
|
||||
|
||||
event.recordNew();
|
||||
if (eventUtils.isEnabled()) eventUtils.fire(event);
|
||||
}
|
||||
|
||||
/** Returns the position of the comment in workspace coordinates. */
|
||||
getRelativeToSurfaceXY(): Coordinate {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
/** Disposes of this comment. */
|
||||
dispose() {
|
||||
this.disposing = true;
|
||||
this.fireDeleteEvent();
|
||||
this.workspace.removeTopComment(this);
|
||||
this.disposed = true;
|
||||
}
|
||||
|
||||
/** Returns whether the comment has been disposed or not. */
|
||||
isDisposed() {
|
||||
return this.disposed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this comment view is currently being disposed or has
|
||||
* already been disposed.
|
||||
*/
|
||||
isDeadOrDying(): boolean {
|
||||
return this.disposing || this.disposed;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {BlockDefinition, Blocks} from './blocks.js';
|
||||
import type {Connection} from './connection.js';
|
||||
import type {Workspace} from './workspace.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
|
||||
/** Database of all workspaces. */
|
||||
const WorkspaceDB_ = Object.create(null);
|
||||
@@ -105,7 +106,18 @@ export function getSelected(): ISelectable | null {
|
||||
* @internal
|
||||
*/
|
||||
export function setSelected(newSelection: ISelectable | null) {
|
||||
if (selected === newSelection) return;
|
||||
|
||||
const event = new (eventUtils.get(eventUtils.SELECTED))(
|
||||
selected?.id ?? null,
|
||||
newSelection?.id ?? null,
|
||||
newSelection?.workspace.id ?? selected?.workspace.id ?? '',
|
||||
);
|
||||
eventUtils.fire(event);
|
||||
|
||||
selected?.unselect();
|
||||
selected = newSelection;
|
||||
selected?.select();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,8 +47,6 @@ export const config: Config = {
|
||||
/**
|
||||
* Maximum misalignment between connections for them to snap together.
|
||||
* This should be the same as the snap radius.
|
||||
*
|
||||
* @deprecated v11 - This is no longer used. Use snapRadius instead.
|
||||
*/
|
||||
connectingSnapRadius: DEFAULT_SNAP_RADIUS,
|
||||
/**
|
||||
|
||||
@@ -153,8 +153,10 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
dispose() {
|
||||
// isConnected returns true for shadows and non-shadows.
|
||||
if (this.isConnected()) {
|
||||
// Destroy the attached shadow block & its children (if it exists).
|
||||
this.setShadowStateInternal();
|
||||
if (this.isSuperior()) {
|
||||
// Destroy the attached shadow block & its children (if it exists).
|
||||
this.setShadowStateInternal();
|
||||
}
|
||||
|
||||
const targetBlock = this.targetBlock();
|
||||
if (targetBlock && !targetBlock.isDeadOrDying()) {
|
||||
@@ -600,6 +602,8 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
this.shadowDom = shadowDom;
|
||||
this.shadowState = shadowState;
|
||||
|
||||
if (this.getSourceBlock().isDeadOrDying()) return;
|
||||
|
||||
const target = this.targetBlock();
|
||||
if (!target) {
|
||||
this.respawnShadow_();
|
||||
@@ -608,7 +612,6 @@ export class Connection implements IASTNodeLocationWithBlock {
|
||||
}
|
||||
} else if (target.isShadow()) {
|
||||
target.dispose(false);
|
||||
if (this.getSourceBlock().isDeadOrDying()) return;
|
||||
this.respawnShadow_();
|
||||
if (this.targetBlock() && this.targetBlock()!.isShadow()) {
|
||||
this.serializeShadow(this.targetBlock());
|
||||
|
||||
@@ -15,3 +15,9 @@ export const COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT';
|
||||
* The language-neutral ID given to the collapsed field.
|
||||
*/
|
||||
export const COLLAPSED_FIELD_NAME = '_TEMP_COLLAPSED_FIELD';
|
||||
|
||||
/**
|
||||
* The language-neutral ID for when the reason why a block is disabled is
|
||||
* because the user manually disabled it, such as via the context menu.
|
||||
*/
|
||||
export const MANUALLY_DISABLED = 'MANUALLY_DISABLED';
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import type {Block} from './block.js';
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import * as clipboard from './clipboard.js';
|
||||
import {config} from './config.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import type {
|
||||
@@ -19,16 +18,13 @@ import type {
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {Menu} from './menu.js';
|
||||
import {MenuItem} from './menuitem.js';
|
||||
import {Msg} from './msg.js';
|
||||
import * as aria from './utils/aria.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import {Rect} from './utils/rect.js';
|
||||
import * as serializationBlocks from './serialization/blocks.js';
|
||||
import * as svgMath from './utils/svg_math.js';
|
||||
import * as WidgetDiv from './widgetdiv.js';
|
||||
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import * as Xml from './xml.js';
|
||||
import * as common from './common.js';
|
||||
|
||||
/**
|
||||
* Which block is the context menu attached to?
|
||||
@@ -68,7 +64,7 @@ let menu_: Menu | null = null;
|
||||
* @param rtl True if RTL, false if LTR.
|
||||
*/
|
||||
export function show(
|
||||
e: Event,
|
||||
e: PointerEvent,
|
||||
options: (ContextMenuOption | LegacyContextMenuOption)[],
|
||||
rtl: boolean,
|
||||
) {
|
||||
@@ -77,7 +73,7 @@ export function show(
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
const menu = populate_(options, rtl);
|
||||
const menu = populate_(options, rtl, e);
|
||||
menu_ = menu;
|
||||
|
||||
position_(menu, e, rtl);
|
||||
@@ -94,11 +90,13 @@ export function show(
|
||||
*
|
||||
* @param options Array of menu options.
|
||||
* @param rtl True if RTL, false if LTR.
|
||||
* @param e The event that triggered the context menu to open.
|
||||
* @returns The menu that will be shown on right click.
|
||||
*/
|
||||
function populate_(
|
||||
options: (ContextMenuOption | LegacyContextMenuOption)[],
|
||||
rtl: boolean,
|
||||
e: PointerEvent,
|
||||
): Menu {
|
||||
/* Here's what one option object looks like:
|
||||
{text: 'Make It So',
|
||||
@@ -123,7 +121,7 @@ function populate_(
|
||||
// will not be expecting a scope parameter, so there should be
|
||||
// no problems. Just assume it is a ContextMenuOption and we'll
|
||||
// pass undefined if it's not.
|
||||
option.callback((option as ContextMenuOption).scope);
|
||||
option.callback((option as ContextMenuOption).scope, e);
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
@@ -261,129 +259,7 @@ export function callbackFactory(
|
||||
if (eventUtils.isEnabled() && !newBlock.isShadow()) {
|
||||
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(newBlock));
|
||||
}
|
||||
newBlock.select();
|
||||
common.setSelected(newBlock);
|
||||
return newBlock;
|
||||
};
|
||||
}
|
||||
|
||||
// Helper functions for creating context menu options.
|
||||
|
||||
/**
|
||||
* Make a context menu option for deleting the current workspace comment.
|
||||
*
|
||||
* @param comment The workspace comment where the
|
||||
* right-click originated.
|
||||
* @returns A menu option,
|
||||
* containing text, enabled, and a callback.
|
||||
* @internal
|
||||
*/
|
||||
export function commentDeleteOption(
|
||||
comment: WorkspaceCommentSvg,
|
||||
): LegacyContextMenuOption {
|
||||
const deleteOption = {
|
||||
text: Msg['REMOVE_COMMENT'],
|
||||
enabled: true,
|
||||
callback: function () {
|
||||
eventUtils.setGroup(true);
|
||||
comment.dispose();
|
||||
eventUtils.setGroup(false);
|
||||
},
|
||||
};
|
||||
return deleteOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a context menu option for duplicating the current workspace comment.
|
||||
*
|
||||
* @param comment The workspace comment where the
|
||||
* right-click originated.
|
||||
* @returns A menu option,
|
||||
* containing text, enabled, and a callback.
|
||||
* @internal
|
||||
*/
|
||||
export function commentDuplicateOption(
|
||||
comment: WorkspaceCommentSvg,
|
||||
): LegacyContextMenuOption {
|
||||
const duplicateOption = {
|
||||
text: Msg['DUPLICATE_COMMENT'],
|
||||
enabled: true,
|
||||
callback: function () {
|
||||
const data = comment.toCopyData();
|
||||
if (!data) return;
|
||||
clipboard.paste(data, comment.workspace);
|
||||
},
|
||||
};
|
||||
return duplicateOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a context menu option for adding a comment on the workspace.
|
||||
*
|
||||
* @param ws The workspace where the right-click
|
||||
* originated.
|
||||
* @param e The right-click mouse event.
|
||||
* @returns A menu option, containing text, enabled, and a callback.
|
||||
* comments are not bundled in.
|
||||
* @internal
|
||||
*/
|
||||
export function workspaceCommentOption(
|
||||
ws: WorkspaceSvg,
|
||||
e: Event,
|
||||
): ContextMenuOption {
|
||||
/**
|
||||
* Helper function to create and position a comment correctly based on the
|
||||
* location of the mouse event.
|
||||
*/
|
||||
function addWsComment() {
|
||||
const comment = new WorkspaceCommentSvg(
|
||||
ws,
|
||||
Msg['WORKSPACE_COMMENT_DEFAULT_TEXT'],
|
||||
WorkspaceCommentSvg.DEFAULT_SIZE,
|
||||
WorkspaceCommentSvg.DEFAULT_SIZE,
|
||||
);
|
||||
|
||||
const injectionDiv = ws.getInjectionDiv();
|
||||
// Bounding rect coordinates are in client coordinates, meaning that they
|
||||
// are in pixels relative to the upper left corner of the visible browser
|
||||
// window. These coordinates change when you scroll the browser window.
|
||||
const boundingRect = injectionDiv.getBoundingClientRect();
|
||||
|
||||
// The client coordinates offset by the injection div's upper left corner.
|
||||
const mouseEvent = e as MouseEvent;
|
||||
const clientOffsetPixels = new Coordinate(
|
||||
mouseEvent.clientX - boundingRect.left,
|
||||
mouseEvent.clientY - boundingRect.top,
|
||||
);
|
||||
|
||||
// The offset in pixels between the main workspace's origin and the upper
|
||||
// left corner of the injection div.
|
||||
const mainOffsetPixels = ws.getOriginOffsetInPixels();
|
||||
|
||||
// The position of the new comment in pixels relative to the origin of the
|
||||
// main workspace.
|
||||
const finalOffset = Coordinate.difference(
|
||||
clientOffsetPixels,
|
||||
mainOffsetPixels,
|
||||
);
|
||||
// The position of the new comment in main workspace coordinates.
|
||||
finalOffset.scale(1 / ws.scale);
|
||||
|
||||
const commentX = finalOffset.x;
|
||||
const commentY = finalOffset.y;
|
||||
comment.moveBy(commentX, commentY);
|
||||
if (ws.rendered) {
|
||||
comment.initSvg();
|
||||
comment.render();
|
||||
comment.select();
|
||||
}
|
||||
}
|
||||
|
||||
const wsCommentOption = {
|
||||
enabled: true,
|
||||
} as ContextMenuOption;
|
||||
wsCommentOption.text = Msg['ADD_COMMENT'];
|
||||
wsCommentOption.callback = function () {
|
||||
addWsComment();
|
||||
};
|
||||
return wsCommentOption;
|
||||
}
|
||||
|
||||
@@ -8,18 +8,22 @@
|
||||
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import * as clipboard from './clipboard.js';
|
||||
import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
|
||||
import {
|
||||
ContextMenuRegistry,
|
||||
RegistryItem,
|
||||
Scope,
|
||||
} from './contextmenu_registry.js';
|
||||
import {MANUALLY_DISABLED} from './constants.js';
|
||||
import * as dialog from './dialog.js';
|
||||
import * as Events from './events/events.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {CommentIcon} from './icons/comment_icon.js';
|
||||
import {Msg} from './msg.js';
|
||||
import {StatementInput} from './renderers/zelos/zelos.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import * as common from './common.js';
|
||||
|
||||
/**
|
||||
* Option to undo previous action.
|
||||
@@ -455,9 +459,9 @@ export function registerCollapseExpandBlock() {
|
||||
export function registerDisable() {
|
||||
const disableOption: RegistryItem = {
|
||||
displayText(scope: Scope) {
|
||||
return scope.block!.isEnabled()
|
||||
? Msg['DISABLE_BLOCK']
|
||||
: Msg['ENABLE_BLOCK'];
|
||||
return scope.block!.hasDisabledReason(MANUALLY_DISABLED)
|
||||
? Msg['ENABLE_BLOCK']
|
||||
: Msg['DISABLE_BLOCK'];
|
||||
},
|
||||
preconditionFn(scope: Scope) {
|
||||
const block = scope.block;
|
||||
@@ -466,7 +470,14 @@ export function registerDisable() {
|
||||
block!.workspace.options.disable &&
|
||||
block!.isEditable()
|
||||
) {
|
||||
if (block!.getInheritedDisabled()) {
|
||||
// Determine whether this block is currently disabled for any reason
|
||||
// other than the manual reason that this context menu item controls.
|
||||
const disabledReasons = block!.getDisabledReasons();
|
||||
const isDisabledForOtherReason =
|
||||
disabledReasons.size >
|
||||
(disabledReasons.has(MANUALLY_DISABLED) ? 1 : 0);
|
||||
|
||||
if (block!.getInheritedDisabled() || isDisabledForOtherReason) {
|
||||
return 'disabled';
|
||||
}
|
||||
return 'enabled';
|
||||
@@ -479,7 +490,10 @@ export function registerDisable() {
|
||||
if (!existingGroup) {
|
||||
eventUtils.setGroup(true);
|
||||
}
|
||||
block!.setEnabled(!block!.isEnabled());
|
||||
block!.setDisabledReason(
|
||||
!block!.hasDisabledReason(MANUALLY_DISABLED),
|
||||
MANUALLY_DISABLED,
|
||||
);
|
||||
eventUtils.setGroup(existingGroup);
|
||||
},
|
||||
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
|
||||
@@ -554,6 +568,106 @@ export function registerHelp() {
|
||||
ContextMenuRegistry.registry.register(helpOption);
|
||||
}
|
||||
|
||||
/** Registers an option for deleting a workspace comment. */
|
||||
export function registerCommentDelete() {
|
||||
const deleteOption: RegistryItem = {
|
||||
displayText: () => Msg['REMOVE_COMMENT'],
|
||||
preconditionFn(scope: Scope) {
|
||||
return scope.comment?.isDeletable() ? 'enabled' : 'hidden';
|
||||
},
|
||||
callback(scope: Scope) {
|
||||
eventUtils.setGroup(true);
|
||||
scope.comment?.dispose();
|
||||
eventUtils.setGroup(false);
|
||||
},
|
||||
scopeType: ContextMenuRegistry.ScopeType.COMMENT,
|
||||
id: 'commentDelete',
|
||||
weight: 6,
|
||||
};
|
||||
ContextMenuRegistry.registry.register(deleteOption);
|
||||
}
|
||||
|
||||
/** Registers an option for duplicating a workspace comment. */
|
||||
export function registerCommentDuplicate() {
|
||||
const duplicateOption: RegistryItem = {
|
||||
displayText: () => Msg['DUPLICATE_COMMENT'],
|
||||
preconditionFn(scope: Scope) {
|
||||
return scope.comment?.isMovable() ? 'enabled' : 'hidden';
|
||||
},
|
||||
callback(scope: Scope) {
|
||||
if (!scope.comment) return;
|
||||
const data = scope.comment.toCopyData();
|
||||
if (!data) return;
|
||||
clipboard.paste(data, scope.comment.workspace);
|
||||
},
|
||||
scopeType: ContextMenuRegistry.ScopeType.COMMENT,
|
||||
id: 'commentDuplicate',
|
||||
weight: 1,
|
||||
};
|
||||
ContextMenuRegistry.registry.register(duplicateOption);
|
||||
}
|
||||
|
||||
/** Registers an option for adding a workspace comment to the workspace. */
|
||||
export function registerCommentCreate() {
|
||||
const createOption: RegistryItem = {
|
||||
displayText: () => Msg['ADD_COMMENT'],
|
||||
preconditionFn: () => 'enabled',
|
||||
callback: (scope: Scope, e: PointerEvent) => {
|
||||
const workspace = scope.workspace;
|
||||
if (!workspace) return;
|
||||
eventUtils.setGroup(true);
|
||||
const comment = new RenderedWorkspaceComment(workspace);
|
||||
comment.setText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']);
|
||||
comment.moveTo(
|
||||
pixelsToWorkspaceCoords(
|
||||
new Coordinate(e.clientX, e.clientY),
|
||||
workspace,
|
||||
),
|
||||
);
|
||||
common.setSelected(comment);
|
||||
eventUtils.setGroup(false);
|
||||
},
|
||||
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
|
||||
id: 'commentCreate',
|
||||
weight: 8,
|
||||
};
|
||||
ContextMenuRegistry.registry.register(createOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts pixel coordinates (relative to the window) to workspace coordinates.
|
||||
*/
|
||||
function pixelsToWorkspaceCoords(
|
||||
pixelCoord: Coordinate,
|
||||
workspace: WorkspaceSvg,
|
||||
): Coordinate {
|
||||
const injectionDiv = workspace.getInjectionDiv();
|
||||
// Bounding rect coordinates are in client coordinates, meaning that they
|
||||
// are in pixels relative to the upper left corner of the visible browser
|
||||
// window. These coordinates change when you scroll the browser window.
|
||||
const boundingRect = injectionDiv.getBoundingClientRect();
|
||||
|
||||
// The client coordinates offset by the injection div's upper left corner.
|
||||
const clientOffsetPixels = new Coordinate(
|
||||
pixelCoord.x - boundingRect.left,
|
||||
pixelCoord.y - boundingRect.top,
|
||||
);
|
||||
|
||||
// The offset in pixels between the main workspace's origin and the upper
|
||||
// left corner of the injection div.
|
||||
const mainOffsetPixels = workspace.getOriginOffsetInPixels();
|
||||
|
||||
// The position of the new comment in pixels relative to the origin of the
|
||||
// main workspace.
|
||||
const finalOffset = Coordinate.difference(
|
||||
clientOffsetPixels,
|
||||
mainOffsetPixels,
|
||||
);
|
||||
// The position of the new comment in main workspace coordinates.
|
||||
finalOffset.scale(1 / workspace.scale);
|
||||
return finalOffset;
|
||||
}
|
||||
|
||||
/** Registers all block-scoped context menu items. */
|
||||
function registerBlockOptions_() {
|
||||
registerDuplicate();
|
||||
@@ -565,6 +679,13 @@ function registerBlockOptions_() {
|
||||
registerHelp();
|
||||
}
|
||||
|
||||
/** Registers all workspace comment related menu items. */
|
||||
export function registerCommentOptions() {
|
||||
registerCommentDuplicate();
|
||||
registerCommentDelete();
|
||||
registerCommentCreate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all default context menu items. This should be called once per
|
||||
* instance of ContextMenuRegistry.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// Former goog.module ID: Blockly.ContextMenuRegistry
|
||||
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
/**
|
||||
@@ -119,6 +120,7 @@ export namespace ContextMenuRegistry {
|
||||
export enum ScopeType {
|
||||
BLOCK = 'block',
|
||||
WORKSPACE = 'workspace',
|
||||
COMMENT = 'comment',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,13 +130,20 @@ export namespace ContextMenuRegistry {
|
||||
export interface Scope {
|
||||
block?: BlockSvg;
|
||||
workspace?: WorkspaceSvg;
|
||||
comment?: RenderedWorkspaceComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* A menu item as entered in the registry.
|
||||
*/
|
||||
export interface RegistryItem {
|
||||
callback: (p1: Scope) => void;
|
||||
/**
|
||||
* @param scope Object that provides a reference to the thing that had its
|
||||
* context menu opened.
|
||||
* @param e The original event that triggered the context menu to open. Not
|
||||
* the event that triggered the click on the option.
|
||||
*/
|
||||
callback: (scope: Scope, e: PointerEvent) => void;
|
||||
scopeType: ScopeType;
|
||||
displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement;
|
||||
preconditionFn: (p1: Scope) => string;
|
||||
@@ -148,7 +157,13 @@ export namespace ContextMenuRegistry {
|
||||
export interface ContextMenuOption {
|
||||
text: string | HTMLElement;
|
||||
enabled: boolean;
|
||||
callback: (p1: Scope) => void;
|
||||
/**
|
||||
* @param scope Object that provides a reference to the thing that had its
|
||||
* context menu opened.
|
||||
* @param e The original event that triggered the context menu to open. Not
|
||||
* the event that triggered the click on the option.
|
||||
*/
|
||||
callback: (scope: Scope, e: PointerEvent) => void;
|
||||
scope: Scope;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
@@ -206,6 +206,9 @@ let content = `
|
||||
.blocklyDragging {
|
||||
cursor: grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
/* Drag surface disables events to not block the toolbox, so we have to
|
||||
* reenable them here for the cursor values to work. */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Changes cursor on mouse down. Not effective in Firefox because of
|
||||
|
||||
@@ -16,6 +16,7 @@ import {BlockSvg} from './block_svg.js';
|
||||
import {DragTarget} from './drag_target.js';
|
||||
import type {IDeleteArea} from './interfaces/i_delete_area.js';
|
||||
import type {IDraggable} from './interfaces/i_draggable.js';
|
||||
import {isDeletable} from './interfaces/i_deletable.js';
|
||||
|
||||
/**
|
||||
* Abstract class for a component that can delete a block or bubble that is
|
||||
@@ -51,17 +52,16 @@ export class DeleteArea extends DragTarget implements IDeleteArea {
|
||||
* before onDragEnter/onDragOver/onDragExit.
|
||||
*
|
||||
* @param element The block or bubble currently being dragged.
|
||||
* @param couldConnect Whether the element could could connect to another.
|
||||
* @returns Whether the element provided would be deleted if dropped on this
|
||||
* area.
|
||||
*/
|
||||
wouldDelete(element: IDraggable, couldConnect: boolean): boolean {
|
||||
wouldDelete(element: IDraggable): boolean {
|
||||
if (element instanceof BlockSvg) {
|
||||
const block = element;
|
||||
const couldDeleteBlock = !block.getParent() && block.isDeletable();
|
||||
this.updateWouldDelete_(couldDeleteBlock && !couldConnect);
|
||||
this.updateWouldDelete_(couldDeleteBlock);
|
||||
} else {
|
||||
this.updateWouldDelete_(element.isDeletable());
|
||||
this.updateWouldDelete_(isDeletable(element) && element.isDeletable());
|
||||
}
|
||||
return this.wouldDelete_;
|
||||
}
|
||||
|
||||
@@ -39,8 +39,9 @@ export class DragTarget implements IDragTarget {
|
||||
*
|
||||
* @param _dragElement The block or bubble currently being dragged.
|
||||
*/
|
||||
onDragEnter(_dragElement: IDraggable) {}
|
||||
// no-op
|
||||
onDragEnter(_dragElement: IDraggable) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles when a cursor with a block or bubble is dragged over this drag
|
||||
@@ -48,24 +49,27 @@ export class DragTarget implements IDragTarget {
|
||||
*
|
||||
* @param _dragElement The block or bubble currently being dragged.
|
||||
*/
|
||||
onDragOver(_dragElement: IDraggable) {}
|
||||
// no-op
|
||||
onDragOver(_dragElement: IDraggable) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles when a cursor with a block or bubble exits this drag target.
|
||||
*
|
||||
* @param _dragElement The block or bubble currently being dragged.
|
||||
*/
|
||||
onDragExit(_dragElement: IDraggable) {}
|
||||
// no-op
|
||||
onDragExit(_dragElement: IDraggable) {
|
||||
// no-op
|
||||
}
|
||||
/**
|
||||
* Handles when a block or bubble is dropped on this component.
|
||||
* Should not handle delete here.
|
||||
*
|
||||
* @param _dragElement The block or bubble currently being dragged.
|
||||
*/
|
||||
onDrop(_dragElement: IDraggable) {}
|
||||
// no-op
|
||||
onDrop(_dragElement: IDraggable) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bounding rectangle of the drag target area in pixel units
|
||||
|
||||
12
core/dragging.ts
Normal file
12
core/dragging.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {Dragger} from './dragging/dragger.js';
|
||||
import {BlockDragStrategy} from './dragging/block_drag_strategy.js';
|
||||
import {BubbleDragStrategy} from './dragging/bubble_drag_strategy.js';
|
||||
import {CommentDragStrategy} from './dragging/comment_drag_strategy.js';
|
||||
|
||||
export {Dragger, BlockDragStrategy, BubbleDragStrategy, CommentDragStrategy};
|
||||
453
core/dragging/block_drag_strategy.ts
Normal file
453
core/dragging/block_drag_strategy.ts
Normal file
@@ -0,0 +1,453 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {IDragStrategy} from '../interfaces/i_draggable.js';
|
||||
import {Coordinate} from '../utils.js';
|
||||
import * as eventUtils from '../events/utils.js';
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import {RenderedConnection} from '../rendered_connection.js';
|
||||
import * as dom from '../utils/dom.js';
|
||||
import * as blockAnimation from '../block_animations.js';
|
||||
import {ConnectionType} from '../connection_type.js';
|
||||
import * as bumpObjects from '../bump_objects.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {IConnectionPreviewer} from '../interfaces/i_connection_previewer.js';
|
||||
import {Connection} from '../connection.js';
|
||||
import type {Block} from '../block.js';
|
||||
import {config} from '../config.js';
|
||||
import type {BlockMove} from '../events/events_block_move.js';
|
||||
import {finishQueuedRenders} from '../render_management.js';
|
||||
import * as layers from '../layers.js';
|
||||
|
||||
/** Represents a nearby valid connection. */
|
||||
interface ConnectionCandidate {
|
||||
/** A connection on the dragging stack that is compatible with neighbour. */
|
||||
local: RenderedConnection;
|
||||
|
||||
/** A nearby connection that is compatible with local. */
|
||||
neighbour: RenderedConnection;
|
||||
|
||||
/** The distance between the local connection and the neighbour connection. */
|
||||
distance: number;
|
||||
}
|
||||
|
||||
export class BlockDragStrategy implements IDragStrategy {
|
||||
private workspace: WorkspaceSvg;
|
||||
|
||||
/** The parent block at the start of the drag. */
|
||||
private startParentConn: RenderedConnection | null = null;
|
||||
|
||||
/**
|
||||
* The child block at the start of the drag. Only gets set if
|
||||
* `healStack` is true.
|
||||
*/
|
||||
private startChildConn: RenderedConnection | null = null;
|
||||
|
||||
private startLoc: Coordinate | null = null;
|
||||
|
||||
private connectionCandidate: ConnectionCandidate | null = null;
|
||||
|
||||
private connectionPreviewer: IConnectionPreviewer | null = null;
|
||||
|
||||
private dragging = false;
|
||||
|
||||
/**
|
||||
* If this is a shadow block, the offset between this block and the parent
|
||||
* block, to add to the drag location. In workspace units.
|
||||
*/
|
||||
private dragOffset = new Coordinate(0, 0);
|
||||
|
||||
constructor(private block: BlockSvg) {
|
||||
this.workspace = block.workspace;
|
||||
}
|
||||
|
||||
/** Returns true if the block is currently movable. False otherwise. */
|
||||
isMovable(): boolean {
|
||||
if (this.block.isShadow()) {
|
||||
return this.block.getParent()?.isMovable() ?? false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.block.isOwnMovable() &&
|
||||
!this.block.isDeadOrDying() &&
|
||||
!this.workspace.options.readOnly &&
|
||||
// We never drag blocks in the flyout, only create new blocks that are
|
||||
// dragged.
|
||||
!this.block.isInFlyout
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles any setup for starting the drag, including disconnecting the block
|
||||
* from any parent blocks.
|
||||
*/
|
||||
startDrag(e?: PointerEvent): void {
|
||||
if (this.block.isShadow()) {
|
||||
this.startDraggingShadow(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragging = true;
|
||||
if (!eventUtils.getGroup()) {
|
||||
eventUtils.setGroup(true);
|
||||
}
|
||||
this.fireDragStartEvent();
|
||||
|
||||
this.startLoc = this.block.getRelativeToSurfaceXY();
|
||||
|
||||
const previewerConstructor = registry.getClassFromOptions(
|
||||
registry.Type.CONNECTION_PREVIEWER,
|
||||
this.workspace.options,
|
||||
);
|
||||
this.connectionPreviewer = new previewerConstructor!(this.block);
|
||||
|
||||
// During a drag there may be a lot of rerenders, but not field changes.
|
||||
// Turn the cache on so we don't do spurious remeasures during the drag.
|
||||
dom.startTextWidthCache();
|
||||
this.workspace.setResizesEnabled(false);
|
||||
blockAnimation.disconnectUiStop();
|
||||
|
||||
const healStack = !!e && (e.altKey || e.ctrlKey || e.metaKey);
|
||||
|
||||
if (this.shouldDisconnect(healStack)) {
|
||||
this.disconnectBlock(healStack);
|
||||
}
|
||||
this.block.setDragging(true);
|
||||
this.workspace.getLayerManager()?.moveToDragLayer(this.block);
|
||||
}
|
||||
|
||||
/** Starts a drag on a shadow, recording the drag offset. */
|
||||
private startDraggingShadow(e?: PointerEvent) {
|
||||
const parent = this.block.getParent();
|
||||
if (!parent) {
|
||||
throw new Error(
|
||||
'Tried to drag a shadow block with no parent. ' +
|
||||
'Shadow blocks should always have parents.',
|
||||
);
|
||||
}
|
||||
this.dragOffset = Coordinate.difference(
|
||||
parent.getRelativeToSurfaceXY(),
|
||||
this.block.getRelativeToSurfaceXY(),
|
||||
);
|
||||
parent.startDrag(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we should disconnect the block when a drag is started.
|
||||
*
|
||||
* @param healStack Whether or not to heal the stack after disconnecting.
|
||||
* @returns True to disconnect the block, false otherwise.
|
||||
*/
|
||||
private shouldDisconnect(healStack: boolean): boolean {
|
||||
return !!(
|
||||
this.block.getParent() ||
|
||||
(healStack &&
|
||||
this.block.nextConnection &&
|
||||
this.block.nextConnection.targetBlock())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the block from any parents. If `healStack` is true and this is
|
||||
* a stack block, we also disconnect from any next blocks and attempt to
|
||||
* attach them to any parent.
|
||||
*
|
||||
* @param healStack Whether or not to heal the stack after disconnecting.
|
||||
*/
|
||||
private disconnectBlock(healStack: boolean) {
|
||||
this.startParentConn =
|
||||
this.block.outputConnection?.targetConnection ??
|
||||
this.block.previousConnection?.targetConnection;
|
||||
if (healStack) {
|
||||
this.startChildConn = this.block.nextConnection?.targetConnection;
|
||||
}
|
||||
|
||||
this.block.unplug(healStack);
|
||||
blockAnimation.disconnectUiEffect(this.block);
|
||||
}
|
||||
|
||||
/** Fire a UI event at the start of a block drag. */
|
||||
private fireDragStartEvent() {
|
||||
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
|
||||
this.block,
|
||||
true,
|
||||
this.block.getDescendants(false),
|
||||
);
|
||||
eventUtils.fire(event);
|
||||
}
|
||||
|
||||
/** Fire a UI event at the end of a block drag. */
|
||||
private fireDragEndEvent() {
|
||||
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
|
||||
this.block,
|
||||
false,
|
||||
this.block.getDescendants(false),
|
||||
);
|
||||
eventUtils.fire(event);
|
||||
}
|
||||
|
||||
/** Fire a move event at the end of a block drag. */
|
||||
private fireMoveEvent() {
|
||||
if (this.block.isDeadOrDying()) return;
|
||||
const event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(
|
||||
this.block,
|
||||
) as BlockMove;
|
||||
event.setReason(['drag']);
|
||||
event.oldCoordinate = this.startLoc!;
|
||||
event.recordNew();
|
||||
eventUtils.fire(event);
|
||||
}
|
||||
|
||||
/** Moves the block and updates any connection previews. */
|
||||
drag(newLoc: Coordinate): void {
|
||||
if (this.block.isShadow()) {
|
||||
this.block.getParent()?.drag(Coordinate.sum(newLoc, this.dragOffset));
|
||||
return;
|
||||
}
|
||||
|
||||
this.block.moveDuringDrag(newLoc);
|
||||
this.updateConnectionPreview(
|
||||
this.block,
|
||||
Coordinate.difference(newLoc, this.startLoc!),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param draggingBlock The block being dragged.
|
||||
* @param delta How far the pointer has moved from the position
|
||||
* at the start of the drag, in workspace units.
|
||||
*/
|
||||
private updateConnectionPreview(draggingBlock: BlockSvg, delta: Coordinate) {
|
||||
const currCandidate = this.connectionCandidate;
|
||||
const newCandidate = this.getConnectionCandidate(draggingBlock, delta);
|
||||
if (!newCandidate) {
|
||||
this.connectionPreviewer!.hidePreview();
|
||||
this.connectionCandidate = null;
|
||||
return;
|
||||
}
|
||||
const candidate =
|
||||
currCandidate &&
|
||||
this.currCandidateIsBetter(currCandidate, delta, newCandidate)
|
||||
? currCandidate
|
||||
: newCandidate;
|
||||
this.connectionCandidate = candidate;
|
||||
|
||||
const {local, neighbour} = candidate;
|
||||
const localIsOutputOrPrevious =
|
||||
local.type === ConnectionType.OUTPUT_VALUE ||
|
||||
local.type === ConnectionType.PREVIOUS_STATEMENT;
|
||||
const neighbourIsConnectedToRealBlock =
|
||||
neighbour.isConnected() && !neighbour.targetBlock()!.isInsertionMarker();
|
||||
if (
|
||||
localIsOutputOrPrevious &&
|
||||
neighbourIsConnectedToRealBlock &&
|
||||
!this.orphanCanConnectAtEnd(
|
||||
draggingBlock,
|
||||
neighbour.targetBlock()!,
|
||||
local.type,
|
||||
)
|
||||
) {
|
||||
this.connectionPreviewer!.previewReplacement(
|
||||
local,
|
||||
neighbour,
|
||||
neighbour.targetBlock()!,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.connectionPreviewer!.previewConnection(local, neighbour);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given orphan block can connect at the end of the
|
||||
* top block's stack or row, false otherwise.
|
||||
*/
|
||||
private orphanCanConnectAtEnd(
|
||||
topBlock: BlockSvg,
|
||||
orphanBlock: BlockSvg,
|
||||
localType: number,
|
||||
): boolean {
|
||||
const orphanConnection =
|
||||
localType === ConnectionType.OUTPUT_VALUE
|
||||
? orphanBlock.outputConnection
|
||||
: orphanBlock.previousConnection;
|
||||
return !!Connection.getConnectionForOrphanedConnection(
|
||||
topBlock as Block,
|
||||
orphanConnection as Connection,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current candidate is better than the new candidate.
|
||||
*
|
||||
* We slightly prefer the current candidate even if it is farther away.
|
||||
*/
|
||||
private currCandidateIsBetter(
|
||||
currCandiate: ConnectionCandidate,
|
||||
delta: Coordinate,
|
||||
newCandidate: ConnectionCandidate,
|
||||
): boolean {
|
||||
const {local: currLocal, neighbour: currNeighbour} = currCandiate;
|
||||
const localPos = new Coordinate(currLocal.x, currLocal.y);
|
||||
const neighbourPos = new Coordinate(currNeighbour.x, currNeighbour.y);
|
||||
const currDistance = Coordinate.distance(
|
||||
Coordinate.sum(localPos, delta),
|
||||
neighbourPos,
|
||||
);
|
||||
return (
|
||||
newCandidate.distance > currDistance - config.currentConnectionPreference
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the closest valid candidate connection, if one can be found.
|
||||
*
|
||||
* Valid neighbour connections are within the configured start radius, with a
|
||||
* compatible type (input, output, etc) and connection check.
|
||||
*/
|
||||
private getConnectionCandidate(
|
||||
draggingBlock: BlockSvg,
|
||||
delta: Coordinate,
|
||||
): ConnectionCandidate | null {
|
||||
const localConns = this.getLocalConnections(draggingBlock);
|
||||
let radius = this.connectionCandidate
|
||||
? config.connectingSnapRadius
|
||||
: config.snapRadius;
|
||||
let candidate = null;
|
||||
|
||||
for (const conn of localConns) {
|
||||
const {connection: neighbour, radius: rad} = conn.closest(radius, delta);
|
||||
if (neighbour) {
|
||||
candidate = {
|
||||
local: conn,
|
||||
neighbour: neighbour,
|
||||
distance: rad,
|
||||
};
|
||||
radius = rad;
|
||||
}
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of the connections we might connect to blocks on the workspace.
|
||||
*
|
||||
* Includes any connections on the dragging block, and any last next
|
||||
* connection on the stack (if one exists).
|
||||
*/
|
||||
private getLocalConnections(draggingBlock: BlockSvg): RenderedConnection[] {
|
||||
const available = draggingBlock.getConnections_(false);
|
||||
const lastOnStack = draggingBlock.lastConnectionInStack(true);
|
||||
if (lastOnStack && lastOnStack !== draggingBlock.nextConnection) {
|
||||
available.push(lastOnStack);
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up any state at the end of the drag. Applies any pending
|
||||
* connections.
|
||||
*/
|
||||
endDrag(e?: PointerEvent): void {
|
||||
if (this.block.isShadow()) {
|
||||
this.block.getParent()?.endDrag(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.fireDragEndEvent();
|
||||
this.fireMoveEvent();
|
||||
|
||||
dom.stopTextWidthCache();
|
||||
|
||||
blockAnimation.disconnectUiStop();
|
||||
this.connectionPreviewer!.hidePreview();
|
||||
|
||||
if (!this.block.isDeadOrDying() && this.dragging) {
|
||||
// These are expensive and don't need to be done if we're deleting, or
|
||||
// if we've already stopped dragging because we moved back to the start.
|
||||
this.workspace
|
||||
.getLayerManager()
|
||||
?.moveOffDragLayer(this.block, layers.BLOCK);
|
||||
this.block.setDragging(false);
|
||||
}
|
||||
|
||||
if (this.connectionCandidate) {
|
||||
// Applying connections also rerenders the relevant blocks.
|
||||
this.applyConnections(this.connectionCandidate);
|
||||
} else {
|
||||
this.block.queueRender();
|
||||
}
|
||||
this.block.snapToGrid();
|
||||
|
||||
// Must dispose after connections are applied to not break the dynamic
|
||||
// connections plugin. See #7859
|
||||
this.connectionPreviewer!.dispose();
|
||||
this.workspace.setResizesEnabled(true);
|
||||
|
||||
eventUtils.setGroup(false);
|
||||
}
|
||||
|
||||
/** Connects the given candidate connections. */
|
||||
private applyConnections(candidate: ConnectionCandidate) {
|
||||
const {local, neighbour} = candidate;
|
||||
local.connect(neighbour);
|
||||
|
||||
const inferiorConnection = local.isSuperior() ? neighbour : local;
|
||||
const rootBlock = this.block.getRootBlock();
|
||||
|
||||
finishQueuedRenders().then(() => {
|
||||
blockAnimation.connectionUiEffect(inferiorConnection.getSourceBlock());
|
||||
// bringToFront is incredibly expensive. Delay until the next frame.
|
||||
setTimeout(() => {
|
||||
rootBlock.bringToFront();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the block back to where it was at the beginning of the drag,
|
||||
* including reconnecting connections.
|
||||
*/
|
||||
revertDrag(): void {
|
||||
if (this.block.isShadow()) {
|
||||
this.block.getParent()?.revertDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
this.startChildConn?.connect(this.block.nextConnection);
|
||||
if (this.startParentConn) {
|
||||
switch (this.startParentConn.type) {
|
||||
case ConnectionType.INPUT_VALUE:
|
||||
this.startParentConn.connect(this.block.outputConnection);
|
||||
break;
|
||||
case ConnectionType.NEXT_STATEMENT:
|
||||
this.startParentConn.connect(this.block.previousConnection);
|
||||
}
|
||||
} else {
|
||||
this.block.moveTo(this.startLoc!, ['drag']);
|
||||
this.workspace
|
||||
.getLayerManager()
|
||||
?.moveOffDragLayer(this.block, layers.BLOCK);
|
||||
// Blocks dragged directly from a flyout may need to be bumped into
|
||||
// bounds.
|
||||
bumpObjects.bumpIntoBounds(
|
||||
this.workspace,
|
||||
this.workspace.getMetricsManager().getScrollMetrics(true),
|
||||
this.block,
|
||||
);
|
||||
}
|
||||
|
||||
this.startChildConn = null;
|
||||
this.startParentConn = null;
|
||||
|
||||
this.connectionPreviewer!.hidePreview();
|
||||
this.connectionCandidate = null;
|
||||
|
||||
this.block.setDragging(false);
|
||||
this.dragging = false;
|
||||
}
|
||||
}
|
||||
52
core/dragging/bubble_drag_strategy.ts
Normal file
52
core/dragging/bubble_drag_strategy.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {IDragStrategy} from '../interfaces/i_draggable.js';
|
||||
import {Coordinate} from '../utils.js';
|
||||
import * as eventUtils from '../events/utils.js';
|
||||
import {IBubble, WorkspaceSvg} from '../blockly.js';
|
||||
import * as layers from '../layers.js';
|
||||
|
||||
export class BubbleDragStrategy implements IDragStrategy {
|
||||
private startLoc: Coordinate | null = null;
|
||||
|
||||
constructor(
|
||||
private bubble: IBubble,
|
||||
private workspace: WorkspaceSvg,
|
||||
) {}
|
||||
|
||||
isMovable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
startDrag(): void {
|
||||
if (!eventUtils.getGroup()) {
|
||||
eventUtils.setGroup(true);
|
||||
}
|
||||
this.startLoc = this.bubble.getRelativeToSurfaceXY();
|
||||
this.workspace.setResizesEnabled(false);
|
||||
this.workspace.getLayerManager()?.moveToDragLayer(this.bubble);
|
||||
this.bubble.setDragging && this.bubble.setDragging(true);
|
||||
}
|
||||
|
||||
drag(newLoc: Coordinate): void {
|
||||
this.bubble.moveDuringDrag(newLoc);
|
||||
}
|
||||
|
||||
endDrag(): void {
|
||||
this.workspace.setResizesEnabled(true);
|
||||
eventUtils.setGroup(false);
|
||||
|
||||
this.workspace
|
||||
.getLayerManager()
|
||||
?.moveOffDragLayer(this.bubble, layers.BUBBLE);
|
||||
this.bubble.setDragging(false);
|
||||
}
|
||||
|
||||
revertDrag(): void {
|
||||
if (this.startLoc) this.bubble.moveDuringDrag(this.startLoc);
|
||||
}
|
||||
}
|
||||
68
core/dragging/comment_drag_strategy.ts
Normal file
68
core/dragging/comment_drag_strategy.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {IDragStrategy} from '../interfaces/i_draggable.js';
|
||||
import {Coordinate} from '../utils.js';
|
||||
import * as eventUtils from '../events/utils.js';
|
||||
import * as layers from '../layers.js';
|
||||
import {RenderedWorkspaceComment} from '../comments.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {CommentMove} from '../events/events_comment_move.js';
|
||||
|
||||
export class CommentDragStrategy implements IDragStrategy {
|
||||
private startLoc: Coordinate | null = null;
|
||||
|
||||
private workspace: WorkspaceSvg;
|
||||
|
||||
constructor(private comment: RenderedWorkspaceComment) {
|
||||
this.workspace = comment.workspace;
|
||||
}
|
||||
|
||||
isMovable(): boolean {
|
||||
return this.comment.isOwnMovable() && !this.workspace.options.readOnly;
|
||||
}
|
||||
|
||||
startDrag(): void {
|
||||
if (!eventUtils.getGroup()) {
|
||||
eventUtils.setGroup(true);
|
||||
}
|
||||
this.startLoc = this.comment.getRelativeToSurfaceXY();
|
||||
this.workspace.setResizesEnabled(false);
|
||||
this.workspace.getLayerManager()?.moveToDragLayer(this.comment);
|
||||
this.comment.setDragging(true);
|
||||
}
|
||||
|
||||
drag(newLoc: Coordinate): void {
|
||||
this.comment.moveDuringDrag(newLoc);
|
||||
}
|
||||
|
||||
endDrag(): void {
|
||||
this.fireMoveEvent();
|
||||
|
||||
this.workspace.setResizesEnabled(true);
|
||||
eventUtils.setGroup(false);
|
||||
|
||||
this.workspace
|
||||
.getLayerManager()
|
||||
?.moveOffDragLayer(this.comment, layers.BLOCK);
|
||||
this.comment.setDragging(false);
|
||||
}
|
||||
|
||||
private fireMoveEvent() {
|
||||
if (this.comment.isDeadOrDying()) return;
|
||||
const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(
|
||||
this.comment,
|
||||
) as CommentMove;
|
||||
event.setReason(['drag']);
|
||||
event.oldCoordinate_ = this.startLoc!;
|
||||
event.recordNew();
|
||||
eventUtils.fire(event);
|
||||
}
|
||||
|
||||
revertDrag(): void {
|
||||
if (this.startLoc) this.comment.moveDuringDrag(this.startLoc);
|
||||
}
|
||||
}
|
||||
142
core/dragging/dragger.ts
Normal file
142
core/dragging/dragger.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {IDragTarget} from '../interfaces/i_drag_target.js';
|
||||
import {IDeletable, isDeletable} from '../interfaces/i_deletable.js';
|
||||
import {IDragger} from '../interfaces/i_dragger.js';
|
||||
import {IDraggable} from '../interfaces/i_draggable.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {ComponentManager} from '../component_manager.js';
|
||||
import {IDeleteArea} from '../interfaces/i_delete_area.js';
|
||||
import * as registry from '../registry.js';
|
||||
|
||||
export class Dragger implements IDragger {
|
||||
protected startLoc: Coordinate;
|
||||
|
||||
protected dragTarget: IDragTarget | null = null;
|
||||
|
||||
constructor(
|
||||
protected draggable: IDraggable,
|
||||
protected workspace: WorkspaceSvg,
|
||||
) {
|
||||
this.startLoc = draggable.getRelativeToSurfaceXY();
|
||||
}
|
||||
|
||||
/** Handles any drag startup. */
|
||||
onDragStart(e: PointerEvent) {
|
||||
this.draggable.startDrag(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles calculating where the element should actually be moved to.
|
||||
*
|
||||
* @param totalDelta The total amount in pixel coordinates the mouse has moved
|
||||
* since the start of the drag.
|
||||
*/
|
||||
onDrag(e: PointerEvent, totalDelta: Coordinate) {
|
||||
this.moveDraggable(e, totalDelta);
|
||||
|
||||
// Must check `wouldDelete` before calling other hooks on drag targets
|
||||
// since we have documented that we would do so.
|
||||
if (isDeletable(this.draggable)) {
|
||||
this.draggable.setDeleteStyle(
|
||||
this.wouldDeleteDraggable(e, this.draggable),
|
||||
);
|
||||
}
|
||||
this.updateDragTarget(e);
|
||||
}
|
||||
|
||||
/** Updates the drag target under the pointer (if there is one). */
|
||||
protected updateDragTarget(e: PointerEvent) {
|
||||
const newDragTarget = this.workspace.getDragTarget(e);
|
||||
if (this.dragTarget !== newDragTarget) {
|
||||
this.dragTarget?.onDragExit(this.draggable);
|
||||
newDragTarget?.onDragEnter(this.draggable);
|
||||
}
|
||||
newDragTarget?.onDragOver(this.draggable);
|
||||
this.dragTarget = newDragTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the correct workspace coordinate for the movable and tells
|
||||
* the draggable to go to that location.
|
||||
*/
|
||||
private moveDraggable(e: PointerEvent, totalDelta: Coordinate) {
|
||||
const delta = this.pixelsToWorkspaceUnits(totalDelta);
|
||||
const newLoc = Coordinate.sum(this.startLoc, delta);
|
||||
this.draggable.drag(newLoc, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we would delete the draggable if it was dropped
|
||||
* at the current location.
|
||||
*/
|
||||
protected wouldDeleteDraggable(
|
||||
e: PointerEvent,
|
||||
draggable: IDraggable & IDeletable,
|
||||
) {
|
||||
const dragTarget = this.workspace.getDragTarget(e);
|
||||
if (!dragTarget) return false;
|
||||
|
||||
const componentManager = this.workspace.getComponentManager();
|
||||
const isDeleteArea = componentManager.hasCapability(
|
||||
dragTarget.id,
|
||||
ComponentManager.Capability.DELETE_AREA,
|
||||
);
|
||||
if (!isDeleteArea) return false;
|
||||
|
||||
return (dragTarget as IDeleteArea).wouldDelete(draggable);
|
||||
}
|
||||
|
||||
/** Handles any drag cleanup. */
|
||||
onDragEnd(e: PointerEvent) {
|
||||
const dragTarget = this.workspace.getDragTarget(e);
|
||||
if (dragTarget) {
|
||||
this.dragTarget?.onDrop(this.draggable);
|
||||
}
|
||||
|
||||
if (this.shouldReturnToStart(e, this.draggable)) {
|
||||
this.draggable.revertDrag();
|
||||
}
|
||||
|
||||
this.draggable.endDrag(e);
|
||||
|
||||
if (
|
||||
isDeletable(this.draggable) &&
|
||||
this.wouldDeleteDraggable(e, this.draggable)
|
||||
) {
|
||||
this.draggable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we should return the draggable to its original location
|
||||
* at the end of the drag.
|
||||
*/
|
||||
protected shouldReturnToStart(e: PointerEvent, draggable: IDraggable) {
|
||||
const dragTarget = this.workspace.getDragTarget(e);
|
||||
if (!dragTarget) return false;
|
||||
return dragTarget.shouldPreventMove(draggable);
|
||||
}
|
||||
|
||||
protected pixelsToWorkspaceUnits(pixelCoord: Coordinate): Coordinate {
|
||||
const result = new Coordinate(
|
||||
pixelCoord.x / this.workspace.scale,
|
||||
pixelCoord.y / this.workspace.scale,
|
||||
);
|
||||
if (this.workspace.isMutator) {
|
||||
// If we're in a mutator, its scale is always 1, purely because of some
|
||||
// oddities in our rendering optimizations. The actual scale is the same
|
||||
// as the scale on the parent workspace. Fix that for dragging.
|
||||
const mainScale = this.workspace.options.parentWorkspace!.scale;
|
||||
result.scale(1 / mainScale);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
registry.register(registry.Type.BLOCK_DRAGGER, registry.DEFAULT, Dragger);
|
||||
@@ -24,6 +24,10 @@ import {CommentChange, CommentChangeJson} from './events_comment_change.js';
|
||||
import {CommentCreate, CommentCreateJson} from './events_comment_create.js';
|
||||
import {CommentDelete} from './events_comment_delete.js';
|
||||
import {CommentMove, CommentMoveJson} from './events_comment_move.js';
|
||||
import {
|
||||
CommentCollapse,
|
||||
CommentCollapseJson,
|
||||
} from './events_comment_collapse.js';
|
||||
import {MarkerMove, MarkerMoveJson} from './events_marker_move.js';
|
||||
import {Selected, SelectedJson} from './events_selected.js';
|
||||
import {ThemeChange, ThemeChangeJson} from './events_theme_change.js';
|
||||
@@ -73,6 +77,8 @@ export {CommentCreateJson};
|
||||
export {CommentDelete};
|
||||
export {CommentMove};
|
||||
export {CommentMoveJson};
|
||||
export {CommentCollapse};
|
||||
export {CommentCollapseJson};
|
||||
export {FinishedLoading};
|
||||
export {MarkerMove};
|
||||
export {MarkerMoveJson};
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {Block} from '../block.js';
|
||||
import type {BlockSvg} from '../block_svg.js';
|
||||
import {IconType} from '../icons/icon_types.js';
|
||||
import {hasBubble} from '../interfaces/i_has_bubble.js';
|
||||
import {MANUALLY_DISABLED} from '../constants.js';
|
||||
import * as registry from '../registry.js';
|
||||
import * as utilsXml from '../utils/xml.js';
|
||||
import {Workspace} from '../workspace.js';
|
||||
@@ -44,6 +45,12 @@ export class BlockChange extends BlockBase {
|
||||
/** The new value of the element. */
|
||||
newValue: unknown;
|
||||
|
||||
/**
|
||||
* If element is 'disabled', this is the language-neutral identifier of the
|
||||
* reason why the block was or was not disabled.
|
||||
*/
|
||||
private disabledReason?: string;
|
||||
|
||||
/**
|
||||
* @param opt_block The changed block. Undefined for a blank event.
|
||||
* @param opt_element One of 'field', 'comment', 'disabled', etc.
|
||||
@@ -86,6 +93,9 @@ export class BlockChange extends BlockBase {
|
||||
json['name'] = this.name;
|
||||
json['oldValue'] = this.oldValue;
|
||||
json['newValue'] = this.newValue;
|
||||
if (this.disabledReason) {
|
||||
json['disabledReason'] = this.disabledReason;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -112,9 +122,30 @@ export class BlockChange extends BlockBase {
|
||||
newEvent.name = json['name'];
|
||||
newEvent.oldValue = json['oldValue'];
|
||||
newEvent.newValue = json['newValue'];
|
||||
if (json['disabledReason'] !== undefined) {
|
||||
newEvent.disabledReason = json['disabledReason'];
|
||||
}
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the language-neutral identifier for the reason why the block was or was
|
||||
* not disabled. This is only valid for events where element is 'disabled'.
|
||||
* Defaults to 'MANUALLY_DISABLED'.
|
||||
*
|
||||
* @param disabledReason The identifier of the reason why the block was or was
|
||||
* not disabled.
|
||||
*/
|
||||
setDisabledReason(disabledReason: string) {
|
||||
if (this.element !== 'disabled') {
|
||||
throw new Error(
|
||||
'Cannot set the disabled reason for a BlockChange event if the ' +
|
||||
'element is not "disabled".',
|
||||
);
|
||||
}
|
||||
this.disabledReason = disabledReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this event record any change of state?
|
||||
*
|
||||
@@ -168,7 +199,10 @@ export class BlockChange extends BlockBase {
|
||||
block.setCollapsed(!!value);
|
||||
break;
|
||||
case 'disabled':
|
||||
block.setEnabled(!value);
|
||||
block.setDisabledReason(
|
||||
!!value,
|
||||
this.disabledReason ?? MANUALLY_DISABLED,
|
||||
);
|
||||
break;
|
||||
case 'inline':
|
||||
block.setInputsInline(!!value);
|
||||
@@ -219,6 +253,7 @@ export interface BlockChangeJson extends BlockBaseJson {
|
||||
name?: string;
|
||||
newValue: unknown;
|
||||
oldValue: unknown;
|
||||
disabledReason?: string;
|
||||
}
|
||||
|
||||
registry.register(registry.Type.EVENT, eventUtils.CHANGE, BlockChange);
|
||||
|
||||
@@ -11,10 +11,8 @@
|
||||
*/
|
||||
// Former goog.module ID: Blockly.Events.CommentBase
|
||||
|
||||
import * as utilsXml from '../utils/xml.js';
|
||||
import type {WorkspaceComment} from '../workspace_comment.js';
|
||||
import * as Xml from '../xml.js';
|
||||
|
||||
import type {WorkspaceComment} from '../comments/workspace_comment.js';
|
||||
import * as comments from '../serialization/workspace_comments.js';
|
||||
import {
|
||||
Abstract as AbstractEvent,
|
||||
AbstractEventJson,
|
||||
@@ -102,12 +100,10 @@ export class CommentBase extends AbstractEvent {
|
||||
) {
|
||||
const workspace = event.getEventWorkspace_();
|
||||
if (create) {
|
||||
const xmlElement = utilsXml.createElement('xml');
|
||||
if (!event.xml) {
|
||||
throw new Error('Ecountered a comment event without proper xml');
|
||||
if (!event.json) {
|
||||
throw new Error('Encountered a comment event without proper json');
|
||||
}
|
||||
xmlElement.appendChild(event.xml);
|
||||
Xml.domToWorkspace(xmlElement, workspace);
|
||||
comments.append(event.json, workspace);
|
||||
} else {
|
||||
if (!event.commentId) {
|
||||
throw new Error(
|
||||
@@ -119,8 +115,7 @@ export class CommentBase extends AbstractEvent {
|
||||
if (comment) {
|
||||
comment.dispose();
|
||||
} else {
|
||||
// Only complain about root-level block.
|
||||
console.warn("Can't uncreate non-existent comment: " + event.commentId);
|
||||
console.warn("Can't delete non-existent comment: " + event.commentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// Former goog.module ID: Blockly.Events.CommentChange
|
||||
|
||||
import * as registry from '../registry.js';
|
||||
import type {WorkspaceComment} from '../workspace_comment.js';
|
||||
import type {WorkspaceComment} from '../comments/workspace_comment.js';
|
||||
|
||||
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
@@ -124,13 +124,16 @@ export class CommentChange extends CommentBase {
|
||||
'the constructor, or call fromJson',
|
||||
);
|
||||
}
|
||||
const comment = workspace.getCommentById(this.commentId);
|
||||
// TODO: Remove the cast when we fix the type of getCommentById.
|
||||
const comment = workspace.getCommentById(
|
||||
this.commentId,
|
||||
) as unknown as WorkspaceComment;
|
||||
if (!comment) {
|
||||
console.warn("Can't change non-existent comment: " + this.commentId);
|
||||
return;
|
||||
}
|
||||
const contents = forward ? this.newContents_ : this.oldContents_;
|
||||
if (!contents) {
|
||||
if (contents === undefined) {
|
||||
if (forward) {
|
||||
throw new Error(
|
||||
'The new contents is undefined. Either pass a value to ' +
|
||||
@@ -142,7 +145,7 @@ export class CommentChange extends CommentBase {
|
||||
'the constructor, or call fromJson',
|
||||
);
|
||||
}
|
||||
comment.setContent(contents);
|
||||
comment.setText(contents);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
103
core/events/events_comment_collapse.ts
Normal file
103
core/events/events_comment_collapse.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as registry from '../registry.js';
|
||||
import {WorkspaceComment} from '../comments/workspace_comment.js';
|
||||
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
export class CommentCollapse extends CommentBase {
|
||||
override type = eventUtils.COMMENT_COLLAPSE;
|
||||
|
||||
constructor(
|
||||
comment?: WorkspaceComment,
|
||||
public newCollapsed?: boolean,
|
||||
) {
|
||||
super(comment);
|
||||
|
||||
if (!comment) {
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the event as JSON.
|
||||
*
|
||||
* @returns JSON representation.
|
||||
*/
|
||||
override toJson(): CommentCollapseJson {
|
||||
const json = super.toJson() as CommentCollapseJson;
|
||||
if (this.newCollapsed === undefined) {
|
||||
throw new Error(
|
||||
'The new collapse value undefined. Either call recordNew, or ' +
|
||||
'call fromJson',
|
||||
);
|
||||
}
|
||||
json['newCollapsed'] = this.newCollapsed;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the JSON event.
|
||||
*
|
||||
* @param event The event to append new properties to. Should be a subclass
|
||||
* of CommentCollapse, 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: CommentCollapseJson,
|
||||
workspace: Workspace,
|
||||
event?: any,
|
||||
): CommentCollapse {
|
||||
const newEvent = super.fromJson(
|
||||
json,
|
||||
workspace,
|
||||
event ?? new CommentCollapse(),
|
||||
) as CommentCollapse;
|
||||
newEvent.newCollapsed = json.newCollapsed;
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a collapse event.
|
||||
*
|
||||
* @param forward True if run forward, false if run backward (undo).
|
||||
*/
|
||||
override run(forward: boolean) {
|
||||
const workspace = this.getEventWorkspace_();
|
||||
if (!this.commentId) {
|
||||
throw new Error(
|
||||
'The comment ID is undefined. Either pass a comment to ' +
|
||||
'the constructor, or call fromJson',
|
||||
);
|
||||
}
|
||||
// TODO: Remove cast when we update getCommentById.
|
||||
const comment = workspace.getCommentById(
|
||||
this.commentId,
|
||||
) as unknown as WorkspaceComment;
|
||||
if (!comment) {
|
||||
console.warn(
|
||||
"Can't collapse or uncollapse non-existent comment: " + this.commentId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
comment.setCollapsed(forward ? !!this.newCollapsed : !this.newCollapsed);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommentCollapseJson extends CommentBaseJson {
|
||||
newCollapsed: boolean;
|
||||
}
|
||||
|
||||
registry.register(
|
||||
registry.Type.EVENT,
|
||||
eventUtils.COMMENT_COLLAPSE,
|
||||
CommentCollapse,
|
||||
);
|
||||
@@ -12,10 +12,10 @@
|
||||
// Former goog.module ID: Blockly.Events.CommentCreate
|
||||
|
||||
import * as registry from '../registry.js';
|
||||
import type {WorkspaceComment} from '../workspace_comment.js';
|
||||
import type {WorkspaceComment} from '../comments/workspace_comment.js';
|
||||
import * as comments from '../serialization/workspace_comments.js';
|
||||
import * as utilsXml from '../utils/xml.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';
|
||||
@@ -29,6 +29,9 @@ export class CommentCreate extends CommentBase {
|
||||
/** The XML representation of the created workspace comment. */
|
||||
xml?: Element | DocumentFragment;
|
||||
|
||||
/** The JSON representation of the created workspace comment. */
|
||||
json?: comments.State;
|
||||
|
||||
/**
|
||||
* @param opt_comment The created comment.
|
||||
* Undefined for a blank event.
|
||||
@@ -37,10 +40,11 @@ export class CommentCreate extends CommentBase {
|
||||
super(opt_comment);
|
||||
|
||||
if (!opt_comment) {
|
||||
return;
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
// Blank event to be populated by fromJson.
|
||||
this.xml = opt_comment.toXmlWithXY();
|
||||
|
||||
this.xml = Xml.saveWorkspaceComment(opt_comment);
|
||||
this.json = comments.save(opt_comment, {addCoordinates: true});
|
||||
}
|
||||
|
||||
// TODO (#1266): "Full" and "minimal" serialization.
|
||||
@@ -57,7 +61,14 @@ export class CommentCreate extends CommentBase {
|
||||
'the constructor, or call fromJson',
|
||||
);
|
||||
}
|
||||
if (!this.json) {
|
||||
throw new Error(
|
||||
'The comment JSON is undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson',
|
||||
);
|
||||
}
|
||||
json['xml'] = Xml.domToText(this.xml);
|
||||
json['json'] = this.json;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -81,6 +92,7 @@ export class CommentCreate extends CommentBase {
|
||||
event ?? new CommentCreate(),
|
||||
) as CommentCreate;
|
||||
newEvent.xml = utilsXml.textToDom(json['xml']);
|
||||
newEvent.json = json['json'];
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
@@ -96,6 +108,7 @@ export class CommentCreate extends CommentBase {
|
||||
|
||||
export interface CommentCreateJson extends CommentBaseJson {
|
||||
xml: string;
|
||||
json: object;
|
||||
}
|
||||
|
||||
registry.register(
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
// Former goog.module ID: Blockly.Events.CommentDelete
|
||||
|
||||
import * as registry from '../registry.js';
|
||||
import type {WorkspaceComment} from '../workspace_comment.js';
|
||||
|
||||
import type {WorkspaceComment} from '../comments/workspace_comment.js';
|
||||
import * as comments from '../serialization/workspace_comments.js';
|
||||
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
import * as utilsXml from '../utils/xml.js';
|
||||
@@ -29,6 +29,9 @@ export class CommentDelete extends CommentBase {
|
||||
/** The XML representation of the deleted workspace comment. */
|
||||
xml?: Element;
|
||||
|
||||
/** The JSON representation of the created workspace comment. */
|
||||
json?: comments.State;
|
||||
|
||||
/**
|
||||
* @param opt_comment The deleted comment.
|
||||
* Undefined for a blank event.
|
||||
@@ -40,7 +43,8 @@ export class CommentDelete extends CommentBase {
|
||||
return; // Blank event to be populated by fromJson.
|
||||
}
|
||||
|
||||
this.xml = opt_comment.toXmlWithXY();
|
||||
this.xml = Xml.saveWorkspaceComment(opt_comment);
|
||||
this.json = comments.save(opt_comment, {addCoordinates: true});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +69,14 @@ export class CommentDelete extends CommentBase {
|
||||
'the constructor, or call fromJson',
|
||||
);
|
||||
}
|
||||
if (!this.json) {
|
||||
throw new Error(
|
||||
'The comment JSON is undefined. Either pass a block to ' +
|
||||
'the constructor, or call fromJson',
|
||||
);
|
||||
}
|
||||
json['xml'] = Xml.domToText(this.xml);
|
||||
json['json'] = this.json;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -89,12 +100,14 @@ export class CommentDelete extends CommentBase {
|
||||
event ?? new CommentDelete(),
|
||||
) as CommentDelete;
|
||||
newEvent.xml = utilsXml.textToDom(json['xml']);
|
||||
newEvent.json = json['json'];
|
||||
return newEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommentDeleteJson extends CommentBaseJson {
|
||||
xml: string;
|
||||
json: object;
|
||||
}
|
||||
|
||||
registry.register(
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
import * as registry from '../registry.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import type {WorkspaceComment} from '../workspace_comment.js';
|
||||
import type {WorkspaceComment} from '../comments/workspace_comment.js';
|
||||
|
||||
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
|
||||
import * as eventUtils from './utils.js';
|
||||
@@ -35,6 +35,17 @@ export class CommentMove extends CommentBase {
|
||||
/** The location of the comment after the move, in workspace coordinates. */
|
||||
newCoordinate_?: Coordinate;
|
||||
|
||||
/**
|
||||
* An explanation of what this move is for. Known values include:
|
||||
* 'drag' -- A drag operation completed.
|
||||
* 'snap' -- Comment got shifted to line up with the grid.
|
||||
* 'inbounds' -- Block got pushed back into a non-scrolling workspace.
|
||||
* 'create' -- Block created via deserialization.
|
||||
* 'cleanup' -- Workspace aligned top-level blocks.
|
||||
* Event merging may create multiple reasons: ['drag', 'inbounds', 'snap'].
|
||||
*/
|
||||
reason?: string[];
|
||||
|
||||
/**
|
||||
* @param opt_comment The comment that is being moved. Undefined for a blank
|
||||
* event.
|
||||
@@ -70,6 +81,15 @@ export class CommentMove extends CommentBase {
|
||||
this.newCoordinate_ = this.comment_.getRelativeToSurfaceXY();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the reason for a move event.
|
||||
*
|
||||
* @param reason Why is this move happening? 'drag', 'bump', 'snap', ...
|
||||
*/
|
||||
setReason(reason: string[]) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the location before the move. Use this if you don't create the
|
||||
* event until the end of the move, but you know the original location.
|
||||
@@ -158,7 +178,10 @@ export class CommentMove extends CommentBase {
|
||||
'the constructor, or call fromJson',
|
||||
);
|
||||
}
|
||||
const comment = workspace.getCommentById(this.commentId);
|
||||
// TODO: Remove cast when we update getCommentById.
|
||||
const comment = workspace.getCommentById(
|
||||
this.commentId,
|
||||
) as unknown as WorkspaceComment;
|
||||
if (!comment) {
|
||||
console.warn("Can't move non-existent comment: " + this.commentId);
|
||||
return;
|
||||
@@ -172,9 +195,7 @@ export class CommentMove extends CommentBase {
|
||||
'or call fromJson',
|
||||
);
|
||||
}
|
||||
// TODO: Check if the comment is being dragged, and give up if so.
|
||||
const current = comment.getRelativeToSurfaceXY();
|
||||
comment.moveBy(target.x - current.x, target.y - current.y);
|
||||
comment.moveTo(target);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -180,11 +180,20 @@ export const COMMENT_CHANGE = 'comment_change';
|
||||
*/
|
||||
export const COMMENT_MOVE = 'comment_move';
|
||||
|
||||
/** Type of event that moves a comment. */
|
||||
export const COMMENT_COLLAPSE = 'comment_collapse';
|
||||
|
||||
/**
|
||||
* Name of event that records a workspace load.
|
||||
*/
|
||||
export const FINISHED_LOADING = 'finished_loading';
|
||||
|
||||
/**
|
||||
* The language-neutral ID for when the reason why a block is disabled is
|
||||
* because the block is not descended from a root block.
|
||||
*/
|
||||
const ORPHANED_BLOCK_DISABLED_REASON = 'ORPHANED_BLOCK';
|
||||
|
||||
/**
|
||||
* Type of events that cause objects to be bumped back into the visible
|
||||
* portion of the workspace.
|
||||
@@ -513,10 +522,8 @@ export function get(
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable a block depending on whether it is properly connected.
|
||||
* Set if a block is disabled depending on whether it is properly connected.
|
||||
* Use this on applications where all blocks should be connected to a top block.
|
||||
* Recommend setting the 'disable' option to 'false' in the config so that
|
||||
* users don't try to re-enable disabled orphan blocks.
|
||||
*
|
||||
* @param event Custom data for event.
|
||||
*/
|
||||
@@ -539,17 +546,20 @@ export function disableOrphans(event: Abstract) {
|
||||
try {
|
||||
recordUndo = false;
|
||||
const parent = block.getParent();
|
||||
if (parent && parent.isEnabled()) {
|
||||
if (
|
||||
parent &&
|
||||
!parent.hasDisabledReason(ORPHANED_BLOCK_DISABLED_REASON)
|
||||
) {
|
||||
const children = block.getDescendants(false);
|
||||
for (let i = 0, child; (child = children[i]); i++) {
|
||||
child.setEnabled(true);
|
||||
child.setDisabledReason(false, ORPHANED_BLOCK_DISABLED_REASON);
|
||||
}
|
||||
} else if (
|
||||
(block.outputConnection || block.previousConnection) &&
|
||||
!eventWorkspace.isDragging()
|
||||
) {
|
||||
do {
|
||||
block.setEnabled(false);
|
||||
block.setDisabledReason(true, ORPHANED_BLOCK_DISABLED_REASON);
|
||||
block = block.getNextBlock();
|
||||
} while (block);
|
||||
}
|
||||
|
||||
@@ -314,6 +314,7 @@ export abstract class Field<T = any>
|
||||
this.setTooltip(this.tooltip_);
|
||||
this.bindEvents_();
|
||||
this.initModel();
|
||||
this.applyColour();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1062,7 +1063,6 @@ export abstract class Field<T = any>
|
||||
this.isDirty_ = true;
|
||||
if (this.sourceBlock_ && this.sourceBlock_.rendered) {
|
||||
(this.sourceBlock_ as BlockSvg).queueRender();
|
||||
(this.sourceBlock_ as BlockSvg).bumpNeighbours();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,612 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2013 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Angle input field.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
// Former goog.module ID: Blockly.FieldAngle
|
||||
|
||||
import {BlockSvg} from './block_svg.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import * as Css from './css.js';
|
||||
import * as dropDownDiv from './dropdowndiv.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {Field, UnattachedFieldError} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {
|
||||
FieldInput,
|
||||
FieldInputConfig,
|
||||
FieldInputValidator,
|
||||
} from './field_input.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import * as math from './utils/math.js';
|
||||
import {Svg} from './utils/svg.js';
|
||||
import * as userAgent from './utils/useragent.js';
|
||||
import * as WidgetDiv from './widgetdiv.js';
|
||||
|
||||
/**
|
||||
* Class for an editable angle field.
|
||||
*/
|
||||
export class FieldAngle extends FieldInput<number> {
|
||||
/** Half the width of protractor image. */
|
||||
static readonly HALF = 100 / 2;
|
||||
|
||||
/**
|
||||
* Radius of protractor circle. Slightly smaller than protractor size since
|
||||
* otherwise SVG crops off half the border at the edges.
|
||||
*/
|
||||
static readonly RADIUS: number = FieldAngle.HALF - 1;
|
||||
|
||||
/**
|
||||
* Default property describing which direction makes an angle field's value
|
||||
* increase. Angle increases clockwise (true) or counterclockwise (false).
|
||||
*/
|
||||
static readonly CLOCKWISE = false;
|
||||
|
||||
/**
|
||||
* The default offset of 0 degrees (and all angles). Always offsets in the
|
||||
* counterclockwise direction, regardless of the field's clockwise property.
|
||||
* Usually either 0 (0 = right) or 90 (0 = up).
|
||||
*/
|
||||
static readonly OFFSET = 0;
|
||||
|
||||
/**
|
||||
* The default maximum angle to allow before wrapping.
|
||||
* Usually either 360 (for 0 to 359.9) or 180 (for -179.9 to 180).
|
||||
*/
|
||||
static readonly WRAP = 360;
|
||||
|
||||
/**
|
||||
* The default amount to round angles to when using a mouse or keyboard nav
|
||||
* input. Must be a positive integer to support keyboard navigation.
|
||||
*/
|
||||
static readonly ROUND = 15;
|
||||
|
||||
/**
|
||||
* Whether the angle should increase as the angle picker is moved clockwise
|
||||
* (true) or counterclockwise (false).
|
||||
*/
|
||||
private clockwise = FieldAngle.CLOCKWISE;
|
||||
|
||||
/**
|
||||
* The offset of zero degrees (and all other angles).
|
||||
*/
|
||||
private offset = FieldAngle.OFFSET;
|
||||
|
||||
/**
|
||||
* The maximum angle to allow before wrapping.
|
||||
*/
|
||||
private wrap = FieldAngle.WRAP;
|
||||
|
||||
/**
|
||||
* The amount to round angles to when using a mouse or keyboard nav input.
|
||||
*/
|
||||
private round = FieldAngle.ROUND;
|
||||
|
||||
/**
|
||||
* Array holding info needed to unbind events.
|
||||
* Used for disposing.
|
||||
* Ex: [[node, name, func], [node, name, func]].
|
||||
*/
|
||||
private boundEvents: browserEvents.Data[] = [];
|
||||
|
||||
/** Dynamic red line pointing at the value's angle. */
|
||||
private line: SVGLineElement | null = null;
|
||||
|
||||
/** Dynamic pink area extending from 0 to the value's angle. */
|
||||
private gauge: SVGPathElement | null = null;
|
||||
|
||||
/** The degree symbol for this field. */
|
||||
protected symbol_: SVGTSpanElement | null = null;
|
||||
|
||||
/**
|
||||
* @param 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
|
||||
* (only used by subclasses that want to handle configuration and setting
|
||||
* the field value after their own constructors have run).
|
||||
* @param validator A function that is called to validate changes to the
|
||||
* field's value. Takes in a number & returns a validated number, or null
|
||||
* to abort the change.
|
||||
* @param 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/angle#creation}
|
||||
* for a list of properties this parameter supports.
|
||||
*/
|
||||
constructor(
|
||||
value?: string | number | typeof Field.SKIP_SETUP,
|
||||
validator?: FieldAngleValidator,
|
||||
config?: FieldAngleConfig,
|
||||
) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
if (value === Field.SKIP_SETUP) return;
|
||||
if (config) {
|
||||
this.configure_(config);
|
||||
}
|
||||
this.setValue(value);
|
||||
if (validator) {
|
||||
this.setValidator(validator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the field based on the given map of options.
|
||||
*
|
||||
* @param config A map of options to configure the field based on.
|
||||
*/
|
||||
protected override configure_(config: FieldAngleConfig) {
|
||||
super.configure_(config);
|
||||
|
||||
switch (config.mode) {
|
||||
case Mode.COMPASS:
|
||||
this.clockwise = true;
|
||||
this.offset = 90;
|
||||
break;
|
||||
case Mode.PROTRACTOR:
|
||||
// This is the default mode, so we could do nothing. But just to
|
||||
// future-proof, we'll set it anyway.
|
||||
this.clockwise = false;
|
||||
this.offset = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Allow individual settings to override the mode setting.
|
||||
if (config.clockwise) this.clockwise = config.clockwise;
|
||||
if (config.offset) this.offset = config.offset;
|
||||
if (config.wrap) this.wrap = config.wrap;
|
||||
if (config.round) this.round = config.round;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the block UI for this field.
|
||||
*/
|
||||
override initView() {
|
||||
super.initView();
|
||||
// Add the degree symbol to the left of the number,
|
||||
// even in RTL (issue #2380).
|
||||
this.symbol_ = dom.createSvgElement(Svg.TSPAN, {});
|
||||
this.symbol_.appendChild(document.createTextNode('°'));
|
||||
this.getTextElement().appendChild(this.symbol_);
|
||||
}
|
||||
|
||||
/** Updates the angle when the field rerenders. */
|
||||
protected override render_() {
|
||||
super.render_();
|
||||
this.updateGraph();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show the angle field's editor.
|
||||
*
|
||||
* @param e Optional mouse event that triggered the field to open,
|
||||
* or undefined if triggered programmatically.
|
||||
*/
|
||||
protected override showEditor_(e?: Event) {
|
||||
// Mobile browsers have issues with in-line textareas (focus & keyboards).
|
||||
const noFocus = userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD;
|
||||
super.showEditor_(e, noFocus);
|
||||
|
||||
const editor = this.dropdownCreate();
|
||||
dropDownDiv.getContentDiv().appendChild(editor);
|
||||
|
||||
if (this.sourceBlock_ instanceof BlockSvg) {
|
||||
dropDownDiv.setColour(
|
||||
this.sourceBlock_.style.colourPrimary,
|
||||
this.sourceBlock_.style.colourTertiary,
|
||||
);
|
||||
}
|
||||
|
||||
dropDownDiv.showPositionedByField(this, this.dropdownDispose.bind(this));
|
||||
|
||||
this.updateGraph();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the angle dropdown editor.
|
||||
*
|
||||
* @returns The newly created slider.
|
||||
*/
|
||||
private dropdownCreate(): SVGSVGElement {
|
||||
const svg = dom.createSvgElement(Svg.SVG, {
|
||||
'xmlns': dom.SVG_NS,
|
||||
'xmlns:html': dom.HTML_NS,
|
||||
'xmlns:xlink': dom.XLINK_NS,
|
||||
'version': '1.1',
|
||||
'height': FieldAngle.HALF * 2 + 'px',
|
||||
'width': FieldAngle.HALF * 2 + 'px',
|
||||
});
|
||||
svg.style.touchAction = 'none';
|
||||
const circle = dom.createSvgElement(
|
||||
Svg.CIRCLE,
|
||||
{
|
||||
'cx': FieldAngle.HALF,
|
||||
'cy': FieldAngle.HALF,
|
||||
'r': FieldAngle.RADIUS,
|
||||
'class': 'blocklyAngleCircle',
|
||||
},
|
||||
svg,
|
||||
);
|
||||
this.gauge = dom.createSvgElement(
|
||||
Svg.PATH,
|
||||
{'class': 'blocklyAngleGauge'},
|
||||
svg,
|
||||
);
|
||||
this.line = dom.createSvgElement(
|
||||
Svg.LINE,
|
||||
{
|
||||
'x1': FieldAngle.HALF,
|
||||
'y1': FieldAngle.HALF,
|
||||
'class': 'blocklyAngleLine',
|
||||
},
|
||||
svg,
|
||||
);
|
||||
// Draw markers around the edge.
|
||||
for (let angle = 0; angle < 360; angle += 15) {
|
||||
dom.createSvgElement(
|
||||
Svg.LINE,
|
||||
{
|
||||
'x1': FieldAngle.HALF + FieldAngle.RADIUS,
|
||||
'y1': FieldAngle.HALF,
|
||||
'x2':
|
||||
FieldAngle.HALF + FieldAngle.RADIUS - (angle % 45 === 0 ? 10 : 5),
|
||||
'y2': FieldAngle.HALF,
|
||||
'class': 'blocklyAngleMarks',
|
||||
'transform':
|
||||
'rotate(' +
|
||||
angle +
|
||||
',' +
|
||||
FieldAngle.HALF +
|
||||
',' +
|
||||
FieldAngle.HALF +
|
||||
')',
|
||||
},
|
||||
svg,
|
||||
);
|
||||
}
|
||||
|
||||
// The angle picker is different from other fields in that it updates on
|
||||
// mousemove even if it's not in the middle of a drag. In future we may
|
||||
// change this behaviour.
|
||||
this.boundEvents.push(
|
||||
browserEvents.conditionalBind(svg, 'click', this, this.hide),
|
||||
);
|
||||
// On touch devices, the picker's value is only updated with a drag. Add
|
||||
// a click handler on the drag surface to update the value if the surface
|
||||
// is clicked.
|
||||
this.boundEvents.push(
|
||||
browserEvents.conditionalBind(
|
||||
circle,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.onMouseMove_,
|
||||
true,
|
||||
),
|
||||
);
|
||||
this.boundEvents.push(
|
||||
browserEvents.conditionalBind(
|
||||
circle,
|
||||
'pointermove',
|
||||
this,
|
||||
this.onMouseMove_,
|
||||
true,
|
||||
),
|
||||
);
|
||||
return svg;
|
||||
}
|
||||
|
||||
/** Disposes of events and DOM-references belonging to the angle editor. */
|
||||
private dropdownDispose() {
|
||||
for (const event of this.boundEvents) {
|
||||
browserEvents.unbind(event);
|
||||
}
|
||||
this.boundEvents.length = 0;
|
||||
this.gauge = null;
|
||||
this.line = null;
|
||||
}
|
||||
|
||||
/** Hide the editor. */
|
||||
private hide() {
|
||||
dropDownDiv.hideIfOwner(this);
|
||||
WidgetDiv.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the angle to match the mouse's position.
|
||||
*
|
||||
* @param e Mouse move event.
|
||||
*/
|
||||
protected onMouseMove_(e: PointerEvent) {
|
||||
// Calculate angle.
|
||||
const bBox = this.gauge!.ownerSVGElement!.getBoundingClientRect();
|
||||
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.
|
||||
return;
|
||||
}
|
||||
angle = math.toDegrees(angle);
|
||||
// 0: East, 90: North, 180: West, 270: South.
|
||||
if (dx < 0) {
|
||||
angle += 180;
|
||||
} else if (dy > 0) {
|
||||
angle += 360;
|
||||
}
|
||||
|
||||
// Do offsetting.
|
||||
if (this.clockwise) {
|
||||
angle = this.offset + 360 - angle;
|
||||
} else {
|
||||
angle = 360 - (this.offset - angle);
|
||||
}
|
||||
|
||||
this.displayMouseOrKeyboardValue(angle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles and displays values that are input via mouse or arrow key input.
|
||||
* These values need to be rounded and wrapped before being displayed so
|
||||
* that the text input's value is appropriate.
|
||||
*
|
||||
* @param angle New angle.
|
||||
*/
|
||||
private displayMouseOrKeyboardValue(angle: number) {
|
||||
if (this.round) {
|
||||
angle = Math.round(angle / this.round) * this.round;
|
||||
}
|
||||
angle = this.wrapValue(angle);
|
||||
if (angle !== this.value_) {
|
||||
// Intermediate value changes from user input are not confirmed until the
|
||||
// user closes the editor, and may be numerous. Inhibit reporting these as
|
||||
// normal block change events, and instead report them as special
|
||||
// intermediate changes that do not get recorded in undo history.
|
||||
const oldValue = this.value_;
|
||||
this.setEditorValue_(angle, /* fireChangeEvent= */ false);
|
||||
if (
|
||||
this.sourceBlock_ &&
|
||||
eventUtils.isEnabled() &&
|
||||
this.value_ !== oldValue
|
||||
) {
|
||||
eventUtils.fire(
|
||||
new (eventUtils.get(eventUtils.BLOCK_FIELD_INTERMEDIATE_CHANGE))(
|
||||
this.sourceBlock_,
|
||||
this.name || null,
|
||||
oldValue,
|
||||
this.value_,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Redraw the graph with the current angle. */
|
||||
private updateGraph() {
|
||||
if (!this.gauge || !this.line) {
|
||||
return;
|
||||
}
|
||||
// Always display the input (i.e. getText) even if it is invalid.
|
||||
let angleDegrees = Number(this.getText()) + this.offset;
|
||||
angleDegrees %= 360;
|
||||
let angleRadians = math.toRadians(angleDegrees);
|
||||
const path = ['M ', FieldAngle.HALF, ',', FieldAngle.HALF];
|
||||
let x2 = FieldAngle.HALF;
|
||||
let y2 = FieldAngle.HALF;
|
||||
if (!isNaN(angleRadians)) {
|
||||
const clockwiseFlag = Number(this.clockwise);
|
||||
const angle1 = math.toRadians(this.offset);
|
||||
const x1 = Math.cos(angle1) * FieldAngle.RADIUS;
|
||||
const y1 = Math.sin(angle1) * -FieldAngle.RADIUS;
|
||||
if (clockwiseFlag) {
|
||||
angleRadians = 2 * angle1 - angleRadians;
|
||||
}
|
||||
x2 += Math.cos(angleRadians) * FieldAngle.RADIUS;
|
||||
y2 -= Math.sin(angleRadians) * FieldAngle.RADIUS;
|
||||
// Don't ask how the flag calculations work. They just do.
|
||||
let largeFlag = Math.abs(
|
||||
Math.floor((angleRadians - angle1) / Math.PI) % 2,
|
||||
);
|
||||
if (clockwiseFlag) {
|
||||
largeFlag = 1 - largeFlag;
|
||||
}
|
||||
path.push(
|
||||
' l ',
|
||||
x1,
|
||||
',',
|
||||
y1,
|
||||
' A ',
|
||||
FieldAngle.RADIUS,
|
||||
',',
|
||||
FieldAngle.RADIUS,
|
||||
' 0 ',
|
||||
largeFlag,
|
||||
' ',
|
||||
clockwiseFlag,
|
||||
' ',
|
||||
x2,
|
||||
',',
|
||||
y2,
|
||||
' z',
|
||||
);
|
||||
}
|
||||
this.gauge.setAttribute('d', path.join(''));
|
||||
this.line.setAttribute('x2', `${x2}`);
|
||||
this.line.setAttribute('y2', `${y2}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key down to the editor.
|
||||
*
|
||||
* @param e Keyboard event.
|
||||
*/
|
||||
protected override onHtmlInputKeyDown_(e: KeyboardEvent) {
|
||||
super.onHtmlInputKeyDown_(e);
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
}
|
||||
|
||||
let multiplier = 0;
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
// decrement (increment in RTL)
|
||||
multiplier = block.RTL ? 1 : -1;
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
// increment (decrement in RTL)
|
||||
multiplier = block.RTL ? -1 : 1;
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
// decrement
|
||||
multiplier = -1;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
// increment
|
||||
multiplier = 1;
|
||||
break;
|
||||
}
|
||||
if (multiplier) {
|
||||
const value = this.getValue() as number;
|
||||
this.displayMouseOrKeyboardValue(value + multiplier * this.round);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the input value is a valid angle.
|
||||
*
|
||||
* @param newValue The input value.
|
||||
* @returns A valid angle, or null if invalid.
|
||||
*/
|
||||
protected override doClassValidation_(newValue?: any): number | null {
|
||||
const value = Number(newValue);
|
||||
if (isNaN(value) || !isFinite(value)) {
|
||||
return null;
|
||||
}
|
||||
return this.wrapValue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the value so that it is in the range (-360 + wrap, wrap).
|
||||
*
|
||||
* @param value The value to wrap.
|
||||
* @returns The wrapped value.
|
||||
*/
|
||||
private wrapValue(value: number): number {
|
||||
value %= 360;
|
||||
if (value < 0) {
|
||||
value += 360;
|
||||
}
|
||||
if (value > this.wrap) {
|
||||
value -= 360;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a FieldAngle from a JSON arg object.
|
||||
*
|
||||
* @param options A JSON object with options (angle).
|
||||
* @returns The new field instance.
|
||||
* @nocollapse
|
||||
* @internal
|
||||
*/
|
||||
static override 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);
|
||||
}
|
||||
}
|
||||
|
||||
fieldRegistry.register('field_angle', FieldAngle);
|
||||
|
||||
FieldAngle.prototype.DEFAULT_VALUE = 0;
|
||||
|
||||
/**
|
||||
* CSS for angle field.
|
||||
*/
|
||||
Css.register(`
|
||||
.blocklyAngleCircle {
|
||||
stroke: #444;
|
||||
stroke-width: 1;
|
||||
fill: #ddd;
|
||||
fill-opacity: 0.8;
|
||||
}
|
||||
|
||||
.blocklyAngleMarks {
|
||||
stroke: #444;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.blocklyAngleGauge {
|
||||
fill: #f88;
|
||||
fill-opacity: 0.8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blocklyAngleLine {
|
||||
stroke: #f00;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
pointer-events: none;
|
||||
}
|
||||
`);
|
||||
|
||||
/**
|
||||
* The two main modes of the angle field.
|
||||
* Compass specifies:
|
||||
* - clockwise: true
|
||||
* - offset: 90
|
||||
* - wrap: 0
|
||||
* - round: 15
|
||||
*
|
||||
* Protractor specifies:
|
||||
* - clockwise: false
|
||||
* - offset: 0
|
||||
* - wrap: 0
|
||||
* - round: 15
|
||||
*/
|
||||
export enum Mode {
|
||||
COMPASS = 'compass',
|
||||
PROTRACTOR = 'protractor',
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra configuration options for the angle field.
|
||||
*/
|
||||
export interface FieldAngleConfig extends FieldInputConfig {
|
||||
mode?: Mode;
|
||||
clockwise?: boolean;
|
||||
offset?: number;
|
||||
wrap?: number;
|
||||
round?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* fromJson configuration options for the angle field.
|
||||
*/
|
||||
export interface FieldAngleFromJsonConfig extends FieldAngleConfig {
|
||||
angle?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that is called to validate changes to the field's value before
|
||||
* they are set.
|
||||
*
|
||||
* @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values}
|
||||
* @param newValue The value to be validated.
|
||||
* @returns One of three instructions for setting the new value: `T`, `null`,
|
||||
* or `undefined`.
|
||||
*
|
||||
* - `T` to set this function's returned value instead of `newValue`.
|
||||
*
|
||||
* - `null` to invoke `doValueInvalid_` and not set a value.
|
||||
*
|
||||
* - `undefined` to set `newValue` as is.
|
||||
*/
|
||||
export type FieldAngleValidator = FieldInputValidator<number>;
|
||||
@@ -1,721 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2012 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Colour input field.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
// Former goog.module ID: Blockly.FieldColour
|
||||
|
||||
// Unused import preserved for side-effects. Remove if unneeded.
|
||||
import './events/events_block_change.js';
|
||||
|
||||
import {BlockSvg} from './block_svg.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import * as Css from './css.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import * as dropDownDiv from './dropdowndiv.js';
|
||||
import {
|
||||
Field,
|
||||
FieldConfig,
|
||||
FieldValidator,
|
||||
UnattachedFieldError,
|
||||
} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import * as aria from './utils/aria.js';
|
||||
import * as colour from './utils/colour.js';
|
||||
import * as idGenerator from './utils/idgenerator.js';
|
||||
import {Size} from './utils/size.js';
|
||||
|
||||
/**
|
||||
* Class for a colour input field.
|
||||
*/
|
||||
export class FieldColour extends Field<string> {
|
||||
/**
|
||||
* An array of colour strings for the palette.
|
||||
* Copied from goog.ui.ColorPicker.SIMPLE_GRID_COLORS
|
||||
* All colour pickers use this unless overridden with setColours.
|
||||
*/
|
||||
// prettier-ignore
|
||||
static COLOURS: string[] = [
|
||||
// grays
|
||||
'#ffffff', '#cccccc', '#c0c0c0', '#999999',
|
||||
'#666666', '#333333', '#000000', // reds
|
||||
'#ffcccc', '#ff6666', '#ff0000', '#cc0000',
|
||||
'#990000', '#660000', '#330000', // oranges
|
||||
'#ffcc99', '#ff9966', '#ff9900', '#ff6600',
|
||||
'#cc6600', '#993300', '#663300', // yellows
|
||||
'#ffff99', '#ffff66', '#ffcc66', '#ffcc33',
|
||||
'#cc9933', '#996633', '#663333', // olives
|
||||
'#ffffcc', '#ffff33', '#ffff00', '#ffcc00',
|
||||
'#999900', '#666600', '#333300', // greens
|
||||
'#99ff99', '#66ff99', '#33ff33', '#33cc00',
|
||||
'#009900', '#006600', '#003300', // turquoises
|
||||
'#99ffff', '#33ffff', '#66cccc', '#00cccc',
|
||||
'#339999', '#336666', '#003333', // blues
|
||||
'#ccffff', '#66ffff', '#33ccff', '#3366ff',
|
||||
'#3333ff', '#000099', '#000066', // purples
|
||||
'#ccccff', '#9999ff', '#6666cc', '#6633ff',
|
||||
'#6600cc', '#333399', '#330099', // violets
|
||||
'#ffccff', '#ff99ff', '#cc66cc', '#cc33cc',
|
||||
'#993399', '#663366', '#330033',
|
||||
];
|
||||
|
||||
/**
|
||||
* An array of tooltip strings for the palette. If not the same length as
|
||||
* COLOURS, the colour's hex code will be used for any missing titles.
|
||||
* All colour pickers use this unless overridden with setColours.
|
||||
*/
|
||||
static TITLES: string[] = [];
|
||||
|
||||
/**
|
||||
* Number of columns in the palette.
|
||||
* All colour pickers use this unless overridden with setColumns.
|
||||
*/
|
||||
static COLUMNS = 7;
|
||||
|
||||
/** The field's colour picker element. */
|
||||
private picker: HTMLElement | null = null;
|
||||
|
||||
/** Index of the currently highlighted element. */
|
||||
private highlightedIndex: number | null = null;
|
||||
|
||||
/**
|
||||
* Array holding info needed to unbind events.
|
||||
* Used for disposing.
|
||||
* Ex: [[node, name, func], [node, name, func]].
|
||||
*/
|
||||
private boundEvents: browserEvents.Data[] = [];
|
||||
|
||||
/**
|
||||
* 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 = 'default';
|
||||
|
||||
/**
|
||||
* Used to tell if the field needs to be rendered the next time the block is
|
||||
* rendered. Colour fields are statically sized, and only need to be
|
||||
* rendered at initialization.
|
||||
*/
|
||||
protected override isDirty_ = false;
|
||||
|
||||
/** Array of colours used by this field. If null, use the global list. */
|
||||
private colours: string[] | null = null;
|
||||
|
||||
/**
|
||||
* Array of colour tooltips used by this field. If null, use the global
|
||||
* list.
|
||||
*/
|
||||
private titles: string[] | null = null;
|
||||
|
||||
/**
|
||||
* Number of colour columns used by this field. If 0, use the global
|
||||
* setting. By default use the global constants for columns.
|
||||
*/
|
||||
private columns = 0;
|
||||
|
||||
/**
|
||||
* @param value The initial value of the field. Should be in '#rrggbb'
|
||||
* format. Defaults to the first value in the default colour array. 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 validator A function that is called to validate changes to the
|
||||
* field's value. Takes in a colour string & returns a validated colour
|
||||
* string ('#rrggbb' format), or null to abort the change.
|
||||
* @param 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/colour}
|
||||
* for a list of properties this parameter supports.
|
||||
*/
|
||||
constructor(
|
||||
value?: string | typeof Field.SKIP_SETUP,
|
||||
validator?: FieldColourValidator,
|
||||
config?: FieldColourConfig,
|
||||
) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
if (value === Field.SKIP_SETUP) return;
|
||||
if (config) {
|
||||
this.configure_(config);
|
||||
}
|
||||
this.setValue(value);
|
||||
if (validator) {
|
||||
this.setValidator(validator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the field based on the given map of options.
|
||||
*
|
||||
* @param config A map of options to configure the field based on.
|
||||
*/
|
||||
protected override configure_(config: FieldColourConfig) {
|
||||
super.configure_(config);
|
||||
if (config.colourOptions) this.colours = config.colourOptions;
|
||||
if (config.colourTitles) this.titles = config.colourTitles;
|
||||
if (config.columns) this.columns = config.columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the block UI for this colour field.
|
||||
*/
|
||||
override initView() {
|
||||
this.size_ = new Size(
|
||||
this.getConstants()!.FIELD_COLOUR_DEFAULT_WIDTH,
|
||||
this.getConstants()!.FIELD_COLOUR_DEFAULT_HEIGHT,
|
||||
);
|
||||
this.createBorderRect_();
|
||||
this.getBorderRect().style['fillOpacity'] = '1';
|
||||
this.getBorderRect().setAttribute('stroke', '#fff');
|
||||
if (this.isFullBlockField()) {
|
||||
this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot();
|
||||
}
|
||||
}
|
||||
|
||||
protected override isFullBlockField(): boolean {
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) throw new UnattachedFieldError();
|
||||
|
||||
const constants = this.getConstants();
|
||||
return block.isSimpleReporter() && !!constants?.FIELD_COLOUR_FULL_BLOCK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates text field to match the colour/style of the block.
|
||||
*/
|
||||
override applyColour() {
|
||||
const block = this.getSourceBlock() as BlockSvg | null;
|
||||
if (!block) throw new UnattachedFieldError();
|
||||
|
||||
if (!this.fieldGroup_) return;
|
||||
|
||||
const borderRect = this.borderRect_;
|
||||
if (!borderRect) {
|
||||
throw new Error('The border rect has not been initialized');
|
||||
}
|
||||
|
||||
if (!this.isFullBlockField()) {
|
||||
borderRect.style.display = 'block';
|
||||
borderRect.style.fill = this.getValue() as string;
|
||||
} else {
|
||||
borderRect.style.display = 'none';
|
||||
// In general, do *not* let fields control the color of blocks. Having the
|
||||
// field control the color is unexpected, and could have performance
|
||||
// impacts.
|
||||
block.pathObject.svgPath.setAttribute('fill', this.getValue() as string);
|
||||
block.pathObject.svgPath.setAttribute('stroke', '#fff');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the height and width of the field.
|
||||
*
|
||||
* This should *in general* be the only place render_ gets called from.
|
||||
*
|
||||
* @returns Height and width.
|
||||
*/
|
||||
override getSize(): Size {
|
||||
if (this.getConstants()?.FIELD_COLOUR_FULL_BLOCK) {
|
||||
// In general, do *not* let fields control the color of blocks. Having the
|
||||
// field control the color is unexpected, and could have performance
|
||||
// impacts.
|
||||
// Full block fields have more control of the block than they should
|
||||
// (i.e. updating fill colour). Whenever we get the size, the field may
|
||||
// no longer be a full-block field, so we need to rerender.
|
||||
this.render_();
|
||||
this.isDirty_ = false;
|
||||
}
|
||||
return super.getSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the colour of the block to reflect whether this is a full
|
||||
* block field or not.
|
||||
*/
|
||||
protected override render_() {
|
||||
super.render_();
|
||||
|
||||
const block = this.getSourceBlock() as BlockSvg | null;
|
||||
if (!block) throw new UnattachedFieldError();
|
||||
// Calling applyColour updates the UI (full-block vs non-full-block) for the
|
||||
// colour field, and the colour of the field/block.
|
||||
block.applyColour();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the size of the field based on whether it is a full block field
|
||||
* or not.
|
||||
*
|
||||
* @param margin margin to use when positioning the field.
|
||||
*/
|
||||
protected updateSize_(margin?: number) {
|
||||
const constants = this.getConstants();
|
||||
let totalWidth;
|
||||
let totalHeight;
|
||||
if (this.isFullBlockField()) {
|
||||
const xOffset = margin ?? 0;
|
||||
totalWidth = xOffset * 2;
|
||||
totalHeight = constants!.FIELD_TEXT_HEIGHT;
|
||||
} else {
|
||||
totalWidth = constants!.FIELD_COLOUR_DEFAULT_WIDTH;
|
||||
totalHeight = constants!.FIELD_COLOUR_DEFAULT_HEIGHT;
|
||||
}
|
||||
|
||||
this.size_.height = totalHeight;
|
||||
this.size_.width = totalWidth;
|
||||
|
||||
this.positionBorderRect_();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the input value is a valid colour.
|
||||
*
|
||||
* @param newValue The input value.
|
||||
* @returns A valid colour, or null if invalid.
|
||||
*/
|
||||
protected override doClassValidation_(newValue?: any): string | null {
|
||||
if (typeof newValue !== 'string') {
|
||||
return null;
|
||||
}
|
||||
return colour.parse(newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text for this field. Used when the block is collapsed.
|
||||
*
|
||||
* @returns Text representing the value of this field.
|
||||
*/
|
||||
override getText(): string {
|
||||
let colour = this.value_ as string;
|
||||
// Try to use #rgb format if possible, rather than #rrggbb.
|
||||
if (/^#(.)\1(.)\2(.)\3$/.test(colour)) {
|
||||
colour = '#' + colour[1] + colour[3] + colour[5];
|
||||
}
|
||||
return colour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom colour grid for this field.
|
||||
*
|
||||
* @param colours Array of colours for this block, or null to use default
|
||||
* (FieldColour.COLOURS).
|
||||
* @param titles Optional array of colour tooltips, or null to use default
|
||||
* (FieldColour.TITLES).
|
||||
* @returns Returns itself (for method chaining).
|
||||
*/
|
||||
setColours(colours: string[], titles?: string[]): FieldColour {
|
||||
this.colours = colours;
|
||||
if (titles) {
|
||||
this.titles = titles;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom grid size for this field.
|
||||
*
|
||||
* @param columns Number of columns for this block, or 0 to use default
|
||||
* (FieldColour.COLUMNS).
|
||||
* @returns Returns itself (for method chaining).
|
||||
*/
|
||||
setColumns(columns: number): FieldColour {
|
||||
this.columns = columns;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Create and show the colour field's editor. */
|
||||
protected override showEditor_() {
|
||||
this.dropdownCreate();
|
||||
dropDownDiv.getContentDiv().appendChild(this.picker!);
|
||||
|
||||
dropDownDiv.showPositionedByField(this, this.dropdownDispose.bind(this));
|
||||
|
||||
// Focus so we can start receiving keyboard events.
|
||||
this.picker!.focus({preventScroll: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a click on a colour cell.
|
||||
*
|
||||
* @param e Mouse event.
|
||||
*/
|
||||
private onClick(e: PointerEvent) {
|
||||
const cell = e.target as Element;
|
||||
const colour = cell && cell.getAttribute('data-colour');
|
||||
if (colour !== null) {
|
||||
this.setValue(colour);
|
||||
dropDownDiv.hideIfOwner(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a key down event. Navigate around the grid with the
|
||||
* arrow keys. Enter selects the highlighted colour.
|
||||
*
|
||||
* @param e Keyboard event.
|
||||
*/
|
||||
private onKeyDown(e: KeyboardEvent) {
|
||||
let handled = true;
|
||||
let highlighted: HTMLElement | null;
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
this.moveHighlightBy(0, -1);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
this.moveHighlightBy(0, 1);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
this.moveHighlightBy(-1, 0);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
this.moveHighlightBy(1, 0);
|
||||
break;
|
||||
case 'Enter':
|
||||
// Select the highlighted colour.
|
||||
highlighted = this.getHighlighted();
|
||||
if (highlighted) {
|
||||
const colour = highlighted.getAttribute('data-colour');
|
||||
if (colour !== null) {
|
||||
this.setValue(colour);
|
||||
}
|
||||
}
|
||||
dropDownDiv.hideWithoutAnimation();
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
if (handled) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the currently highlighted position by dx and dy.
|
||||
*
|
||||
* @param dx Change of x.
|
||||
* @param dy Change of y.
|
||||
*/
|
||||
private moveHighlightBy(dx: number, dy: number) {
|
||||
if (!this.highlightedIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const colours = this.colours || FieldColour.COLOURS;
|
||||
const columns = this.columns || FieldColour.COLUMNS;
|
||||
|
||||
// Get the current x and y coordinates.
|
||||
let x = this.highlightedIndex % columns;
|
||||
let y = Math.floor(this.highlightedIndex / columns);
|
||||
|
||||
// Add the offset.
|
||||
x += dx;
|
||||
y += dy;
|
||||
|
||||
if (dx < 0) {
|
||||
// Move left one grid cell, even in RTL.
|
||||
// Loop back to the end of the previous row if we have room.
|
||||
if (x < 0 && y > 0) {
|
||||
x = columns - 1;
|
||||
y--;
|
||||
} else if (x < 0) {
|
||||
x = 0;
|
||||
}
|
||||
} else if (dx > 0) {
|
||||
// Move right one grid cell, even in RTL.
|
||||
// Loop to the start of the next row, if there's room.
|
||||
if (x > columns - 1 && y < Math.floor(colours.length / columns) - 1) {
|
||||
x = 0;
|
||||
y++;
|
||||
} else if (x > columns - 1) {
|
||||
x--;
|
||||
}
|
||||
} else if (dy < 0) {
|
||||
// Move up one grid cell, stop at the top.
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
}
|
||||
} else if (dy > 0) {
|
||||
// Move down one grid cell, stop at the bottom.
|
||||
if (y > Math.floor(colours.length / columns) - 1) {
|
||||
y = Math.floor(colours.length / columns) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Move the highlight to the new coordinates.
|
||||
const cell = this.picker!.childNodes[y].childNodes[x] as Element;
|
||||
const index = y * columns + x;
|
||||
this.setHighlightedCell(cell, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a mouse move event. Highlight the hovered colour.
|
||||
*
|
||||
* @param e Mouse event.
|
||||
*/
|
||||
private onMouseMove(e: PointerEvent) {
|
||||
const cell = e.target as Element;
|
||||
const index = cell && Number(cell.getAttribute('data-index'));
|
||||
if (index !== null && index !== this.highlightedIndex) {
|
||||
this.setHighlightedCell(cell, index);
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle a mouse enter event. Focus the picker. */
|
||||
private onMouseEnter() {
|
||||
this.picker?.focus({preventScroll: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a mouse leave event. Blur the picker and unhighlight
|
||||
* the currently highlighted colour.
|
||||
*/
|
||||
private onMouseLeave() {
|
||||
this.picker?.blur();
|
||||
const highlighted = this.getHighlighted();
|
||||
if (highlighted) {
|
||||
dom.removeClass(highlighted, 'blocklyColourHighlighted');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently highlighted item (if any).
|
||||
*
|
||||
* @returns Highlighted item (null if none).
|
||||
*/
|
||||
private getHighlighted(): HTMLElement | null {
|
||||
if (!this.highlightedIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columns = this.columns || FieldColour.COLUMNS;
|
||||
const x = this.highlightedIndex % columns;
|
||||
const y = Math.floor(this.highlightedIndex / columns);
|
||||
const row = this.picker!.childNodes[y];
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
return row.childNodes[x] as HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the currently highlighted cell.
|
||||
*
|
||||
* @param cell The new cell to highlight.
|
||||
* @param index The index of the new cell.
|
||||
*/
|
||||
private setHighlightedCell(cell: Element, index: number) {
|
||||
// Unhighlight the current item.
|
||||
const highlighted = this.getHighlighted();
|
||||
if (highlighted) {
|
||||
dom.removeClass(highlighted, 'blocklyColourHighlighted');
|
||||
}
|
||||
// Highlight new item.
|
||||
dom.addClass(cell, 'blocklyColourHighlighted');
|
||||
// Set new highlighted index.
|
||||
this.highlightedIndex = index;
|
||||
|
||||
// Update accessibility roles.
|
||||
const cellId = cell.getAttribute('id');
|
||||
if (cellId && this.picker) {
|
||||
aria.setState(this.picker, aria.State.ACTIVEDESCENDANT, cellId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a colour picker dropdown editor. */
|
||||
private dropdownCreate() {
|
||||
const columns = this.columns || FieldColour.COLUMNS;
|
||||
const colours = this.colours || FieldColour.COLOURS;
|
||||
const titles = this.titles || FieldColour.TITLES;
|
||||
const selectedColour = this.getValue();
|
||||
// Create the palette.
|
||||
const table = document.createElement('table');
|
||||
table.className = 'blocklyColourTable';
|
||||
table.tabIndex = 0;
|
||||
table.dir = 'ltr';
|
||||
aria.setRole(table, aria.Role.GRID);
|
||||
aria.setState(table, aria.State.EXPANDED, true);
|
||||
aria.setState(
|
||||
table,
|
||||
aria.State.ROWCOUNT,
|
||||
Math.floor(colours.length / columns),
|
||||
);
|
||||
aria.setState(table, aria.State.COLCOUNT, columns);
|
||||
let row: Element;
|
||||
for (let i = 0; i < colours.length; i++) {
|
||||
if (i % columns === 0) {
|
||||
row = document.createElement('tr');
|
||||
aria.setRole(row, aria.Role.ROW);
|
||||
table.appendChild(row);
|
||||
}
|
||||
const cell = document.createElement('td');
|
||||
row!.appendChild(cell);
|
||||
// This becomes the value, if clicked.
|
||||
cell.setAttribute('data-colour', colours[i]);
|
||||
cell.title = titles[i] || colours[i];
|
||||
cell.id = idGenerator.getNextUniqueId();
|
||||
cell.setAttribute('data-index', `${i}`);
|
||||
aria.setRole(cell, aria.Role.GRIDCELL);
|
||||
aria.setState(cell, aria.State.LABEL, colours[i]);
|
||||
aria.setState(cell, aria.State.SELECTED, colours[i] === selectedColour);
|
||||
cell.style.backgroundColor = colours[i];
|
||||
if (colours[i] === selectedColour) {
|
||||
cell.className = 'blocklyColourSelected';
|
||||
this.highlightedIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Configure event handler on the table to listen for any event in a cell.
|
||||
this.boundEvents.push(
|
||||
browserEvents.conditionalBind(
|
||||
table,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.onClick,
|
||||
true,
|
||||
),
|
||||
);
|
||||
this.boundEvents.push(
|
||||
browserEvents.conditionalBind(
|
||||
table,
|
||||
'pointermove',
|
||||
this,
|
||||
this.onMouseMove,
|
||||
true,
|
||||
),
|
||||
);
|
||||
this.boundEvents.push(
|
||||
browserEvents.conditionalBind(
|
||||
table,
|
||||
'pointerenter',
|
||||
this,
|
||||
this.onMouseEnter,
|
||||
true,
|
||||
),
|
||||
);
|
||||
this.boundEvents.push(
|
||||
browserEvents.conditionalBind(
|
||||
table,
|
||||
'pointerleave',
|
||||
this,
|
||||
this.onMouseLeave,
|
||||
true,
|
||||
),
|
||||
);
|
||||
this.boundEvents.push(
|
||||
browserEvents.conditionalBind(
|
||||
table,
|
||||
'keydown',
|
||||
this,
|
||||
this.onKeyDown,
|
||||
false,
|
||||
),
|
||||
);
|
||||
|
||||
this.picker = table;
|
||||
}
|
||||
|
||||
/** Disposes of events and DOM-references belonging to the colour editor. */
|
||||
private dropdownDispose() {
|
||||
for (const event of this.boundEvents) {
|
||||
browserEvents.unbind(event);
|
||||
}
|
||||
this.boundEvents.length = 0;
|
||||
this.picker = null;
|
||||
this.highlightedIndex = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a FieldColour from a JSON arg object.
|
||||
*
|
||||
* @param options A JSON object with options (colour).
|
||||
* @returns The new field instance.
|
||||
* @nocollapse
|
||||
* @internal
|
||||
*/
|
||||
static override fromJson(options: FieldColourFromJsonConfig): FieldColour {
|
||||
// `this` might be a subclass of FieldColour if that class doesn't override
|
||||
// the static fromJson method.
|
||||
return new this(options.colour, undefined, options);
|
||||
}
|
||||
}
|
||||
|
||||
/** The default value for this field. */
|
||||
FieldColour.prototype.DEFAULT_VALUE = FieldColour.COLOURS[0];
|
||||
|
||||
fieldRegistry.register('field_colour', FieldColour);
|
||||
|
||||
/**
|
||||
* CSS for colour picker.
|
||||
*/
|
||||
Css.register(`
|
||||
.blocklyColourTable {
|
||||
border-collapse: collapse;
|
||||
display: block;
|
||||
outline: none;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.blocklyColourTable>tr>td {
|
||||
border: 0.5px solid #888;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.blocklyColourTable>tr>td.blocklyColourHighlighted {
|
||||
border-color: #eee;
|
||||
box-shadow: 2px 2px 7px 2px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blocklyColourSelected, .blocklyColourSelected:hover {
|
||||
border-color: #eee !important;
|
||||
outline: 1px solid #333;
|
||||
position: relative;
|
||||
}
|
||||
`);
|
||||
|
||||
/**
|
||||
* Config options for the colour field.
|
||||
*/
|
||||
export interface FieldColourConfig extends FieldConfig {
|
||||
colourOptions?: string[];
|
||||
colourTitles?: string[];
|
||||
columns?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* fromJson config options for the colour field.
|
||||
*/
|
||||
export interface FieldColourFromJsonConfig extends FieldColourConfig {
|
||||
colour?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that is called to validate changes to the field's value before
|
||||
* they are set.
|
||||
*
|
||||
* @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values}
|
||||
* @param newValue The value to be validated.
|
||||
* @returns One of three instructions for setting the new value: `T`, `null`,
|
||||
* or `undefined`.
|
||||
*
|
||||
* - `T` to set this function's returned value instead of `newValue`.
|
||||
*
|
||||
* - `null` to invoke `doValueInvalid_` and not set a value.
|
||||
*
|
||||
* - `undefined` to set `newValue` as is.
|
||||
*/
|
||||
export type FieldColourValidator = FieldValidator<string>;
|
||||
@@ -284,7 +284,7 @@ export interface FieldImageConfig extends FieldConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* fromJson config options for the colour field.
|
||||
* fromJson config options for the image field.
|
||||
*/
|
||||
export interface FieldImageFromJsonConfig extends FieldImageConfig {
|
||||
src?: string;
|
||||
|
||||
@@ -1,526 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Text Area field.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
// Former goog.module ID: Blockly.FieldMultilineInput
|
||||
|
||||
import * as Css from './css.js';
|
||||
import {Field, UnattachedFieldError} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {
|
||||
FieldTextInput,
|
||||
FieldTextInputConfig,
|
||||
FieldTextInputValidator,
|
||||
} from './field_textinput.js';
|
||||
import * as aria from './utils/aria.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import * as parsing from './utils/parsing.js';
|
||||
import {Svg} from './utils/svg.js';
|
||||
import * as userAgent from './utils/useragent.js';
|
||||
import * as WidgetDiv from './widgetdiv.js';
|
||||
|
||||
/**
|
||||
* Class for an editable text area field.
|
||||
*/
|
||||
export class FieldMultilineInput extends FieldTextInput {
|
||||
/**
|
||||
* The SVG group element that will contain a text element for each text row
|
||||
* when initialized.
|
||||
*/
|
||||
textGroup: SVGGElement | null = null;
|
||||
|
||||
/**
|
||||
* Defines the maximum number of lines of field.
|
||||
* If exceeded, scrolling functionality is enabled.
|
||||
*/
|
||||
protected maxLines_ = Infinity;
|
||||
|
||||
/** Whether Y overflow is currently occurring. */
|
||||
protected isOverflowedY_ = false;
|
||||
|
||||
/**
|
||||
* @param value The initial content 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 validator An optional function that is called to validate any
|
||||
* constraints on what the user entered. Takes the new text as an
|
||||
* argument and returns either the accepted text, a replacement text, or
|
||||
* null to abort the change.
|
||||
* @param 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/multiline-text-input#creation}
|
||||
* for a list of properties this parameter supports.
|
||||
*/
|
||||
constructor(
|
||||
value?: string | typeof Field.SKIP_SETUP,
|
||||
validator?: FieldMultilineInputValidator,
|
||||
config?: FieldMultilineInputConfig,
|
||||
) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
if (value === Field.SKIP_SETUP) return;
|
||||
if (config) {
|
||||
this.configure_(config);
|
||||
}
|
||||
this.setValue(value);
|
||||
if (validator) {
|
||||
this.setValidator(validator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the field based on the given map of options.
|
||||
*
|
||||
* @param config A map of options to configure the field based on.
|
||||
*/
|
||||
protected override configure_(config: FieldMultilineInputConfig) {
|
||||
super.configure_(config);
|
||||
if (config.maxLines) this.setMaxLines(config.maxLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this field's value to XML. Should only be called by Blockly.Xml.
|
||||
*
|
||||
* @param fieldElement The element to populate with info about the field's
|
||||
* state.
|
||||
* @returns The element containing info about the field's state.
|
||||
* @internal
|
||||
*/
|
||||
override toXml(fieldElement: Element): Element {
|
||||
// Replace '\n' characters with HTML-escaped equivalent '
'. This is
|
||||
// needed so the plain-text representation of the XML produced by
|
||||
// `Blockly.Xml.domToText` will appear on a single line (this is a
|
||||
// limitation of the plain-text format).
|
||||
fieldElement.textContent = (this.getValue() as string).replace(
|
||||
/\n/g,
|
||||
' ',
|
||||
);
|
||||
return fieldElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field's value based on the given XML element. Should only be
|
||||
* called by Blockly.Xml.
|
||||
*
|
||||
* @param fieldElement The element containing info about the field's state.
|
||||
* @internal
|
||||
*/
|
||||
override fromXml(fieldElement: Element) {
|
||||
this.setValue(fieldElement.textContent!.replace(/ /g, '\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this field's value.
|
||||
* This function only exists for subclasses of FieldMultilineInput which
|
||||
* predate the load/saveState API and only define to/fromXml.
|
||||
*
|
||||
* @returns The state of this field.
|
||||
* @internal
|
||||
*/
|
||||
override saveState(): AnyDuringMigration {
|
||||
const legacyState = this.saveLegacyState(FieldMultilineInput);
|
||||
if (legacyState !== null) {
|
||||
return legacyState;
|
||||
}
|
||||
return this.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field's value based on the given state.
|
||||
* This function only exists for subclasses of FieldMultilineInput which
|
||||
* predate the load/saveState API and only define to/fromXml.
|
||||
*
|
||||
* @param state The state of the variable to assign to this variable field.
|
||||
* @internal
|
||||
*/
|
||||
override loadState(state: AnyDuringMigration) {
|
||||
if (this.loadLegacyState(Field, state)) {
|
||||
return;
|
||||
}
|
||||
this.setValue(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the block UI for this field.
|
||||
*/
|
||||
override initView() {
|
||||
this.createBorderRect_();
|
||||
this.textGroup = dom.createSvgElement(
|
||||
Svg.G,
|
||||
{
|
||||
'class': 'blocklyEditableText',
|
||||
},
|
||||
this.fieldGroup_,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text from this field as displayed on screen. May differ from
|
||||
* getText due to ellipsis, and other formatting.
|
||||
*
|
||||
* @returns Currently displayed text.
|
||||
*/
|
||||
protected override getDisplayText_(): string {
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
}
|
||||
let textLines = this.getText();
|
||||
if (!textLines) {
|
||||
// Prevent the field from disappearing if empty.
|
||||
return Field.NBSP;
|
||||
}
|
||||
const lines = textLines.split('\n');
|
||||
textLines = '';
|
||||
const displayLinesNumber = this.isOverflowedY_
|
||||
? this.maxLines_
|
||||
: lines.length;
|
||||
for (let i = 0; i < displayLinesNumber; i++) {
|
||||
let text = lines[i];
|
||||
if (text.length > this.maxDisplayLength) {
|
||||
// Truncate displayed string and add an ellipsis ('...').
|
||||
text = text.substring(0, this.maxDisplayLength - 4) + '...';
|
||||
} else if (this.isOverflowedY_ && i === displayLinesNumber - 1) {
|
||||
text = text.substring(0, text.length - 3) + '...';
|
||||
}
|
||||
// Replace whitespace with non-breaking spaces so the text doesn't
|
||||
// collapse.
|
||||
text = text.replace(/\s/g, Field.NBSP);
|
||||
|
||||
textLines += text;
|
||||
if (i !== displayLinesNumber - 1) {
|
||||
textLines += '\n';
|
||||
}
|
||||
}
|
||||
if (block.RTL) {
|
||||
// The SVG is LTR, force value to be RTL.
|
||||
textLines += '\u200F';
|
||||
}
|
||||
return textLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_). Is being redefined here to update
|
||||
* overflow state of the field.
|
||||
*
|
||||
* @param newValue The value to be saved. The default validator guarantees
|
||||
* that this is a string.
|
||||
*/
|
||||
protected override doValueUpdate_(newValue: string) {
|
||||
super.doValueUpdate_(newValue);
|
||||
if (this.value_ !== null) {
|
||||
this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_;
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the text of the textElement. */
|
||||
protected override render_() {
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
}
|
||||
// Remove all text group children.
|
||||
let currentChild;
|
||||
const textGroup = this.textGroup;
|
||||
while ((currentChild = textGroup!.firstChild)) {
|
||||
textGroup!.removeChild(currentChild);
|
||||
}
|
||||
|
||||
// Add in text elements into the group.
|
||||
const lines = this.getDisplayText_().split('\n');
|
||||
let y = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const lineHeight =
|
||||
this.getConstants()!.FIELD_TEXT_HEIGHT +
|
||||
this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING;
|
||||
const span = dom.createSvgElement(
|
||||
Svg.TEXT,
|
||||
{
|
||||
'class': 'blocklyText blocklyMultilineText',
|
||||
'x': this.getConstants()!.FIELD_BORDER_RECT_X_PADDING,
|
||||
'y': y + this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING,
|
||||
'dy': this.getConstants()!.FIELD_TEXT_BASELINE,
|
||||
},
|
||||
textGroup,
|
||||
);
|
||||
span.appendChild(document.createTextNode(lines[i]));
|
||||
y += lineHeight;
|
||||
}
|
||||
|
||||
if (this.isBeingEdited_) {
|
||||
const htmlInput = this.htmlInput_ as HTMLElement;
|
||||
if (this.isOverflowedY_) {
|
||||
dom.addClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
|
||||
} else {
|
||||
dom.removeClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
|
||||
}
|
||||
}
|
||||
|
||||
this.updateSize_();
|
||||
|
||||
if (this.isBeingEdited_) {
|
||||
if (block.RTL) {
|
||||
// in RTL, we need to let the browser reflow before resizing
|
||||
// in order to get the correct bounding box of the borderRect
|
||||
// avoiding issue #2777.
|
||||
setTimeout(this.resizeEditor_.bind(this), 0);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the size of the field based on the text. */
|
||||
protected override updateSize_() {
|
||||
const nodes = (this.textGroup as SVGElement).childNodes;
|
||||
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE;
|
||||
const fontWeight = this.getConstants()!.FIELD_TEXT_FONTWEIGHT;
|
||||
const fontFamily = this.getConstants()!.FIELD_TEXT_FONTFAMILY;
|
||||
let totalWidth = 0;
|
||||
let totalHeight = 0;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const tspan = nodes[i] as SVGTextElement;
|
||||
const textWidth = dom.getFastTextWidth(
|
||||
tspan,
|
||||
fontSize,
|
||||
fontWeight,
|
||||
fontFamily,
|
||||
);
|
||||
if (textWidth > totalWidth) {
|
||||
totalWidth = textWidth;
|
||||
}
|
||||
totalHeight +=
|
||||
this.getConstants()!.FIELD_TEXT_HEIGHT +
|
||||
(i > 0 ? this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING : 0);
|
||||
}
|
||||
if (this.isBeingEdited_) {
|
||||
// The default width is based on the longest line in the display text,
|
||||
// but when it's being edited, width should be calculated based on the
|
||||
// absolute longest line, even if it would be truncated after editing.
|
||||
// Otherwise we would get wrong editor width when there are more
|
||||
// lines than this.maxLines_.
|
||||
const actualEditorLines = String(this.value_).split('\n');
|
||||
const dummyTextElement = dom.createSvgElement(Svg.TEXT, {
|
||||
'class': 'blocklyText blocklyMultilineText',
|
||||
});
|
||||
|
||||
for (let i = 0; i < actualEditorLines.length; i++) {
|
||||
if (actualEditorLines[i].length > this.maxDisplayLength) {
|
||||
actualEditorLines[i] = actualEditorLines[i].substring(
|
||||
0,
|
||||
this.maxDisplayLength,
|
||||
);
|
||||
}
|
||||
dummyTextElement.textContent = actualEditorLines[i];
|
||||
const lineWidth = dom.getFastTextWidth(
|
||||
dummyTextElement,
|
||||
fontSize,
|
||||
fontWeight,
|
||||
fontFamily,
|
||||
);
|
||||
if (lineWidth > totalWidth) {
|
||||
totalWidth = lineWidth;
|
||||
}
|
||||
}
|
||||
|
||||
const scrollbarWidth =
|
||||
this.htmlInput_!.offsetWidth - this.htmlInput_!.clientWidth;
|
||||
totalWidth += scrollbarWidth;
|
||||
}
|
||||
if (this.borderRect_) {
|
||||
totalHeight += this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING * 2;
|
||||
totalWidth += this.getConstants()!.FIELD_BORDER_RECT_X_PADDING * 2;
|
||||
this.borderRect_.setAttribute('width', `${totalWidth}`);
|
||||
this.borderRect_.setAttribute('height', `${totalHeight}`);
|
||||
}
|
||||
this.size_.width = totalWidth;
|
||||
this.size_.height = totalHeight;
|
||||
|
||||
this.positionBorderRect_();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the inline free-text editor on top of the text.
|
||||
* Overrides the default behaviour to force rerender in order to
|
||||
* correct block size, based on editor text.
|
||||
*
|
||||
* @param e Optional mouse event that triggered the field to open, or
|
||||
* undefined if triggered programmatically.
|
||||
* @param quietInput True if editor should be created without focus.
|
||||
* Defaults to false.
|
||||
*/
|
||||
override showEditor_(e?: Event, quietInput?: boolean) {
|
||||
super.showEditor_(e, quietInput);
|
||||
this.forceRerender();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the text input editor widget.
|
||||
*
|
||||
* @returns The newly created text input editor.
|
||||
*/
|
||||
protected override widgetCreate_(): HTMLTextAreaElement {
|
||||
const div = WidgetDiv.getDiv();
|
||||
const scale = this.workspace_!.getScale();
|
||||
|
||||
const htmlInput = document.createElement('textarea');
|
||||
htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput';
|
||||
htmlInput.setAttribute('spellcheck', String(this.spellcheck_));
|
||||
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt';
|
||||
div!.style.fontSize = fontSize;
|
||||
htmlInput.style.fontSize = fontSize;
|
||||
const borderRadius = FieldTextInput.BORDERRADIUS * scale + 'px';
|
||||
htmlInput.style.borderRadius = borderRadius;
|
||||
const paddingX = this.getConstants()!.FIELD_BORDER_RECT_X_PADDING * scale;
|
||||
const paddingY =
|
||||
(this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING * scale) / 2;
|
||||
htmlInput.style.padding =
|
||||
paddingY + 'px ' + paddingX + 'px ' + paddingY + 'px ' + paddingX + 'px';
|
||||
const lineHeight =
|
||||
this.getConstants()!.FIELD_TEXT_HEIGHT +
|
||||
this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING;
|
||||
htmlInput.style.lineHeight = lineHeight * scale + 'px';
|
||||
|
||||
div!.appendChild(htmlInput);
|
||||
|
||||
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
|
||||
htmlInput.setAttribute('data-untyped-default-value', String(this.value_));
|
||||
htmlInput.setAttribute('data-old-value', '');
|
||||
if (userAgent.GECKO) {
|
||||
// In FF, ensure the browser reflows before resizing to avoid issue #2777.
|
||||
setTimeout(this.resizeEditor_.bind(this), 0);
|
||||
} else {
|
||||
this.resizeEditor_();
|
||||
}
|
||||
|
||||
this.bindInputEvents_(htmlInput);
|
||||
|
||||
return htmlInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maxLines config for this field.
|
||||
*
|
||||
* @param maxLines Defines the maximum number of lines allowed, before
|
||||
* scrolling functionality is enabled.
|
||||
*/
|
||||
setMaxLines(maxLines: number) {
|
||||
if (
|
||||
typeof maxLines === 'number' &&
|
||||
maxLines > 0 &&
|
||||
maxLines !== this.maxLines_
|
||||
) {
|
||||
this.maxLines_ = maxLines;
|
||||
this.forceRerender();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maxLines config of this field.
|
||||
*
|
||||
* @returns The maxLines config value.
|
||||
*/
|
||||
getMaxLines(): number {
|
||||
return this.maxLines_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key down to the editor. Override the text input definition of this
|
||||
* so as to not close the editor when enter is typed in.
|
||||
*
|
||||
* @param e Keyboard event.
|
||||
*/
|
||||
protected override onHtmlInputKeyDown_(e: KeyboardEvent) {
|
||||
if (e.key !== 'Enter') {
|
||||
super.onHtmlInputKeyDown_(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a FieldMultilineInput from a JSON arg object,
|
||||
* dereferencing any string table references.
|
||||
*
|
||||
* @param options A JSON object with options (text, and spellcheck).
|
||||
* @returns The new field instance.
|
||||
* @nocollapse
|
||||
* @internal
|
||||
*/
|
||||
static override fromJson(
|
||||
options: FieldMultilineInputFromJsonConfig,
|
||||
): FieldMultilineInput {
|
||||
const text = parsing.replaceMessageReferences(options.text);
|
||||
// `this` might be a subclass of FieldMultilineInput if that class doesn't
|
||||
// override the static fromJson method.
|
||||
return new this(text, undefined, options);
|
||||
}
|
||||
}
|
||||
|
||||
fieldRegistry.register('field_multilinetext', FieldMultilineInput);
|
||||
|
||||
/**
|
||||
* CSS for multiline field.
|
||||
*/
|
||||
Css.register(`
|
||||
.blocklyHtmlTextAreaInput {
|
||||
font-family: monospace;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.blocklyHtmlTextAreaInputOverflowedY {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
`);
|
||||
|
||||
/**
|
||||
* Config options for the multiline input field.
|
||||
*/
|
||||
export interface FieldMultilineInputConfig extends FieldTextInputConfig {
|
||||
maxLines?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* fromJson config options for the multiline input field.
|
||||
*/
|
||||
export interface FieldMultilineInputFromJsonConfig
|
||||
extends FieldMultilineInputConfig {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that is called to validate changes to the field's value before
|
||||
* they are set.
|
||||
*
|
||||
* @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/fields/validators#return_values}
|
||||
* @param newValue The value to be validated.
|
||||
* @returns One of three instructions for setting the new value: `T`, `null`,
|
||||
* or `undefined`.
|
||||
*
|
||||
* - `T` to set this function's returned value instead of `newValue`.
|
||||
*
|
||||
* - `null` to invoke `doValueInvalid_` and not set a value.
|
||||
*
|
||||
* - `undefined` to set `newValue` as is.
|
||||
*/
|
||||
export type FieldMultilineInputValidator = FieldTextInputValidator;
|
||||
@@ -22,6 +22,7 @@ import * as eventUtils from './events/utils.js';
|
||||
import {FlyoutButton} from './flyout_button.js';
|
||||
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
|
||||
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||
import {MANUALLY_DISABLED} from './constants.js';
|
||||
import type {Options} from './options.js';
|
||||
import {ScrollbarPair} from './scrollbar_pair.js';
|
||||
import * as blocks from './serialization/blocks.js';
|
||||
@@ -43,6 +44,13 @@ enum FlyoutItemType {
|
||||
BUTTON = 'button',
|
||||
}
|
||||
|
||||
/**
|
||||
* The language-neutral ID for when the reason why a block is disabled is
|
||||
* because the workspace is at block capacity.
|
||||
*/
|
||||
const WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON =
|
||||
'WORKSPACE_AT_BLOCK_CAPACITY';
|
||||
|
||||
/**
|
||||
* Class for a flyout.
|
||||
*/
|
||||
@@ -828,6 +836,12 @@ export abstract class Flyout
|
||||
blockInfo['enabled'] =
|
||||
blockInfo['disabled'] !== 'true' && blockInfo['disabled'] !== true;
|
||||
}
|
||||
if (
|
||||
blockInfo['disabledReasons'] === undefined &&
|
||||
blockInfo['enabled'] === false
|
||||
) {
|
||||
blockInfo['disabledReasons'] = [MANUALLY_DISABLED];
|
||||
}
|
||||
block = blocks.appendInternal(
|
||||
blockInfo as blocks.State,
|
||||
this.workspace_,
|
||||
@@ -1230,7 +1244,10 @@ export abstract class Flyout
|
||||
common.getBlockTypeCounts(block),
|
||||
);
|
||||
while (block) {
|
||||
block.setEnabled(enable);
|
||||
block.setDisabledReason(
|
||||
!enable,
|
||||
WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON,
|
||||
);
|
||||
block = block.getNextBlock();
|
||||
}
|
||||
}
|
||||
@@ -1275,8 +1292,8 @@ export abstract class Flyout
|
||||
}
|
||||
|
||||
// Clone the block.
|
||||
const json = blocks.save(oldBlock) as blocks.State;
|
||||
// Normallly this resizes leading to weird jumps. Save it for terminateDrag.
|
||||
const json = this.serializeBlock(oldBlock);
|
||||
// Normally this resizes leading to weird jumps. Save it for terminateDrag.
|
||||
targetWorkspace.setResizesEnabled(false);
|
||||
const block = blocks.append(json, targetWorkspace) as BlockSvg;
|
||||
|
||||
@@ -1285,6 +1302,16 @@ export abstract class Flyout
|
||||
return block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a block to JSON.
|
||||
*
|
||||
* @param block The block to serialize.
|
||||
* @returns A serialized representation of the block.
|
||||
*/
|
||||
protected serializeBlock(block: BlockSvg): blocks.State {
|
||||
return blocks.save(block) as blocks.State;
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions a block on the target workspace.
|
||||
*
|
||||
|
||||
@@ -16,7 +16,6 @@ import type {Block} from './block.js';
|
||||
import * as common from './common.js';
|
||||
import {Names, NameType} from './names.js';
|
||||
import type {Workspace} from './workspace.js';
|
||||
import {warn} from './utils/deprecation.js';
|
||||
|
||||
/**
|
||||
* Deprecated, no-longer used type declaration for per-block-type generator
|
||||
@@ -255,16 +254,7 @@ export class CodeGenerator {
|
||||
|
||||
// Look up block generator function in dictionary - but fall back
|
||||
// to looking up on this if not found, for backwards compatibility.
|
||||
let func = this.forBlock[block.type];
|
||||
if (!func && (this as any)[block.type]) {
|
||||
warn(
|
||||
'block generator functions on CodeGenerator objects',
|
||||
'10.0',
|
||||
'11.0',
|
||||
'the .forBlock[blockType] dictionary',
|
||||
);
|
||||
func = (this as any)[block.type];
|
||||
}
|
||||
const func = this.forBlock[block.type];
|
||||
if (typeof func !== 'function') {
|
||||
throw Error(
|
||||
`${this.name_} generator does not know how to generate code ` +
|
||||
|
||||
284
core/gesture.ts
284
core/gesture.ts
@@ -18,24 +18,23 @@ import './events/events_click.js';
|
||||
import * as blockAnimations from './block_animations.js';
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
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';
|
||||
import type {IBubble} from './interfaces/i_bubble.js';
|
||||
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||
import * as internalConstants from './internal_constants.js';
|
||||
import * as registry from './registry.js';
|
||||
import * as Tooltip from './tooltip.js';
|
||||
import * as Touch from './touch.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
|
||||
import {WorkspaceDragger} from './workspace_dragger.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import type {IIcon} from './interfaces/i_icon.js';
|
||||
import {IDragger} from './interfaces/i_dragger.js';
|
||||
import * as registry from './registry.js';
|
||||
import {IDraggable, isDraggable} from './interfaces/i_draggable.js';
|
||||
import {RenderedWorkspaceComment} from './comments.js';
|
||||
|
||||
/**
|
||||
* Note: In this file "start" refers to pointerdown
|
||||
@@ -84,6 +83,12 @@ export class Gesture {
|
||||
*/
|
||||
private startBlock: BlockSvg | null = null;
|
||||
|
||||
/**
|
||||
* The comment that the gesture started on, or null if it did not start on a
|
||||
* comment.
|
||||
*/
|
||||
private startComment: RenderedWorkspaceComment | null = null;
|
||||
|
||||
/**
|
||||
* The block that this gesture targets. If the gesture started on a
|
||||
* shadow block, this is the first non-shadow parent of the block. If the
|
||||
@@ -113,11 +118,7 @@ export class Gesture {
|
||||
*/
|
||||
private boundEvents: browserEvents.Data[] = [];
|
||||
|
||||
/** The object tracking a bubble drag, or null if none is in progress. */
|
||||
private bubbleDragger: BubbleDragger | null = null;
|
||||
|
||||
/** The object tracking a block drag, or null if none is in progress. */
|
||||
private blockDragger: IBlockDragger | null = null;
|
||||
private dragger: IDragger | null = null;
|
||||
|
||||
/**
|
||||
* The object tracking a workspace or flyout workspace drag, or null if none
|
||||
@@ -125,6 +126,9 @@ export class Gesture {
|
||||
*/
|
||||
private workspaceDragger: WorkspaceDragger | null = null;
|
||||
|
||||
/** Whether the gesture is dragging or not. */
|
||||
private dragging: boolean = false;
|
||||
|
||||
/** The flyout a gesture started in, if any. */
|
||||
private flyout: IFlyout | null = null;
|
||||
|
||||
@@ -136,7 +140,6 @@ export class Gesture {
|
||||
|
||||
/** Boolean used internally to break a cycle in disposal. */
|
||||
protected isEnding_ = false;
|
||||
private healStack: boolean;
|
||||
|
||||
/** The event that most recently updated this gesture. */
|
||||
private mostRecentEvent: PointerEvent;
|
||||
@@ -185,12 +188,6 @@ export class Gesture {
|
||||
* (0, 0) is at this.mouseDownXY_.
|
||||
*/
|
||||
this.currentDragDeltaXY = new Coordinate(0, 0);
|
||||
|
||||
/**
|
||||
* Boolean used to indicate whether or not to heal the stack after
|
||||
* disconnecting a block.
|
||||
*/
|
||||
this.healStack = !internalConstants.DRAG_STACK;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,9 +206,6 @@ export class Gesture {
|
||||
}
|
||||
this.boundEvents.length = 0;
|
||||
|
||||
if (this.blockDragger) {
|
||||
this.blockDragger.dispose();
|
||||
}
|
||||
if (this.workspaceDragger) {
|
||||
this.workspaceDragger.dispose();
|
||||
}
|
||||
@@ -227,7 +221,7 @@ export class Gesture {
|
||||
const changed = this.updateDragDelta(currentXY);
|
||||
// Exceeded the drag radius for the first time.
|
||||
if (changed) {
|
||||
this.updateIsDragging();
|
||||
this.updateIsDragging(e);
|
||||
Touch.longStop();
|
||||
}
|
||||
this.mostRecentEvent = e;
|
||||
@@ -294,54 +288,7 @@ export class Gesture {
|
||||
// The start block is no longer relevant, because this is a drag.
|
||||
this.startBlock = null;
|
||||
this.targetBlock = this.flyout.createBlock(this.targetBlock);
|
||||
this.targetBlock.select();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this gesture to record whether a bubble is being dragged.
|
||||
* 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.
|
||||
*
|
||||
* @returns True if a bubble is being dragged.
|
||||
*/
|
||||
private updateIsDraggingBubble(): boolean {
|
||||
if (!this.startBubble) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.startDraggingBubble();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether to start a block drag. If a block should be dragged, either
|
||||
* from the flyout or in the workspace, create the necessary BlockDragger and
|
||||
* start the drag.
|
||||
*
|
||||
* 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
|
||||
* drag.
|
||||
*
|
||||
* @returns True if a block is being dragged.
|
||||
*/
|
||||
private updateIsDraggingBlock(): boolean {
|
||||
if (!this.targetBlock) {
|
||||
return false;
|
||||
}
|
||||
if (this.flyout) {
|
||||
if (this.updateIsDraggingFromFlyout()) {
|
||||
this.startDraggingBlock();
|
||||
return true;
|
||||
}
|
||||
} else if (this.targetBlock.isMovable()) {
|
||||
this.startDraggingBlock();
|
||||
common.setSelected(this.targetBlock);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -369,6 +316,7 @@ export class Gesture {
|
||||
: this.startWorkspace_ && this.startWorkspace_.isDraggable();
|
||||
if (!wsMovable) return;
|
||||
|
||||
this.dragging = true;
|
||||
this.workspaceDragger = new WorkspaceDragger(this.startWorkspace_);
|
||||
|
||||
this.workspaceDragger.startDrag();
|
||||
@@ -380,66 +328,43 @@ export class Gesture {
|
||||
* the drag radius is exceeded. It should be called no more than once per
|
||||
* gesture.
|
||||
*/
|
||||
private updateIsDragging() {
|
||||
// Sanity check.
|
||||
private updateIsDragging(e: PointerEvent) {
|
||||
if (!this.startWorkspace_) {
|
||||
throw new Error(
|
||||
'Cannot update dragging because the start workspace is undefined',
|
||||
);
|
||||
}
|
||||
|
||||
if (this.calledUpdateIsDragging) {
|
||||
throw Error('updateIsDragging_ should only be called once per gesture.');
|
||||
}
|
||||
this.calledUpdateIsDragging = true;
|
||||
|
||||
// First check if it was a bubble drag. Bubbles always sit on top of
|
||||
// blocks.
|
||||
if (this.updateIsDraggingBubble()) {
|
||||
return;
|
||||
// If we drag a block out of the flyout, it updates `common.getSelected`
|
||||
// to return the new block.
|
||||
if (this.flyout) this.updateIsDraggingFromFlyout();
|
||||
|
||||
const selected = common.getSelected();
|
||||
if (selected && isDraggable(selected) && selected.isMovable()) {
|
||||
this.dragging = true;
|
||||
this.dragger = this.createDragger(selected, this.startWorkspace_);
|
||||
this.dragger.onDragStart(e);
|
||||
this.dragger.onDrag(e, this.currentDragDeltaXY);
|
||||
} else {
|
||||
this.updateIsDraggingWorkspace();
|
||||
}
|
||||
// Then check if it was a block drag.
|
||||
if (this.updateIsDraggingBlock()) {
|
||||
return;
|
||||
}
|
||||
// Then check if it's a workspace drag.
|
||||
this.updateIsDraggingWorkspace();
|
||||
}
|
||||
|
||||
/** Create a block dragger and start dragging the selected block. */
|
||||
private startDraggingBlock() {
|
||||
const BlockDraggerClass = registry.getClassFromOptions(
|
||||
private createDragger(
|
||||
draggable: IDraggable,
|
||||
workspace: WorkspaceSvg,
|
||||
): IDragger {
|
||||
const DraggerClass = registry.getClassFromOptions(
|
||||
registry.Type.BLOCK_DRAGGER,
|
||||
this.creatorWorkspace.options,
|
||||
true,
|
||||
);
|
||||
|
||||
this.blockDragger = new BlockDraggerClass!(
|
||||
this.targetBlock,
|
||||
this.startWorkspace_,
|
||||
);
|
||||
this.blockDragger!.startDrag(this.currentDragDeltaXY, this.healStack);
|
||||
this.blockDragger!.drag(this.mostRecentEvent, this.currentDragDeltaXY);
|
||||
}
|
||||
|
||||
/** Create a bubble dragger and start dragging the selected bubble. */
|
||||
private startDraggingBubble() {
|
||||
if (!this.startBubble) {
|
||||
throw new Error(
|
||||
'Cannot update dragging the bubble because the start ' +
|
||||
'bubble is undefined',
|
||||
);
|
||||
}
|
||||
if (!this.startWorkspace_) {
|
||||
throw new Error(
|
||||
'Cannot update dragging the bubble because the start ' +
|
||||
'workspace is undefined',
|
||||
);
|
||||
}
|
||||
|
||||
this.bubbleDragger = new BubbleDragger(
|
||||
this.startBubble,
|
||||
this.startWorkspace_,
|
||||
);
|
||||
this.bubbleDragger.startBubbleDrag();
|
||||
this.bubbleDragger.dragBubble(
|
||||
this.mostRecentEvent,
|
||||
this.currentDragDeltaXY,
|
||||
);
|
||||
return new DraggerClass!(draggable, workspace);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -487,10 +412,6 @@ export class Gesture {
|
||||
|
||||
Tooltip.block();
|
||||
|
||||
if (this.targetBlock) {
|
||||
this.targetBlock.select();
|
||||
}
|
||||
|
||||
if (browserEvents.isRightButton(e)) {
|
||||
this.handleRightClick(e);
|
||||
return;
|
||||
@@ -501,7 +422,6 @@ export class Gesture {
|
||||
}
|
||||
|
||||
this.mouseDownXY = new Coordinate(e.clientX, e.clientY);
|
||||
this.healStack = e.altKey || e.ctrlKey || e.metaKey;
|
||||
|
||||
this.bindMouseEvents(e);
|
||||
|
||||
@@ -581,13 +501,8 @@ export class Gesture {
|
||||
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,
|
||||
);
|
||||
} else if (this.dragger) {
|
||||
this.dragger.onDrag(this.mostRecentEvent, this.currentDragDeltaXY);
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -623,15 +538,14 @@ export class Gesture {
|
||||
// than clicks. Fields and icons 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);
|
||||
if (this.dragger) {
|
||||
this.dragger.onDragEnd(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();
|
||||
// Do nothing, bubbles don't currently respond to clicks.
|
||||
} else if (this.isCommentClick()) {
|
||||
// Do nothing, comments don't currently respond to clicks.
|
||||
} else if (this.isFieldClick()) {
|
||||
this.doFieldClick();
|
||||
} else if (this.isIconClick()) {
|
||||
@@ -786,13 +700,8 @@ export class Gesture {
|
||||
return;
|
||||
}
|
||||
Touch.longStop();
|
||||
if (this.bubbleDragger) {
|
||||
this.bubbleDragger.endBubbleDrag(
|
||||
this.mostRecentEvent,
|
||||
this.currentDragDeltaXY,
|
||||
);
|
||||
} else if (this.blockDragger) {
|
||||
this.blockDragger.endDrag(this.mostRecentEvent, this.currentDragDeltaXY);
|
||||
if (this.dragger) {
|
||||
this.dragger.onDragEnd(this.mostRecentEvent, this.currentDragDeltaXY);
|
||||
} else if (this.workspaceDragger) {
|
||||
this.workspaceDragger.endDrag(this.currentDragDeltaXY);
|
||||
}
|
||||
@@ -812,6 +721,9 @@ export class Gesture {
|
||||
this.targetBlock.showContextMenu(e);
|
||||
} else if (this.startBubble) {
|
||||
this.startBubble.showContextMenu(e);
|
||||
} else if (this.startComment) {
|
||||
this.startComment.workspace.hideChaff();
|
||||
this.startComment.showContextMenu(e);
|
||||
} else if (this.startWorkspace_ && !this.flyout) {
|
||||
this.startWorkspace_.hideChaff();
|
||||
this.startWorkspace_.showContextMenu(e);
|
||||
@@ -840,6 +752,13 @@ export class Gesture {
|
||||
}
|
||||
this.setStartWorkspace(ws);
|
||||
this.mostRecentEvent = e;
|
||||
|
||||
if (!this.startBlock && !this.startBubble && !this.startComment) {
|
||||
// Selection determines what things start drags. So to drag the workspace,
|
||||
// we need to deselect anything that was previously selected.
|
||||
common.setSelected(null);
|
||||
}
|
||||
|
||||
this.doStart(e);
|
||||
}
|
||||
|
||||
@@ -908,19 +827,28 @@ export class Gesture {
|
||||
this.mostRecentEvent = e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a pointerdown event on a workspace comment.
|
||||
*
|
||||
* @param e A pointerdown event.
|
||||
* @param comment The comment the event hit.
|
||||
* @internal
|
||||
*/
|
||||
handleCommentStart(e: PointerEvent, comment: RenderedWorkspaceComment) {
|
||||
if (this.gestureHasStarted) {
|
||||
throw Error(
|
||||
'Tried to call gesture.handleCommentStart, ' +
|
||||
'but the gesture had already been started.',
|
||||
);
|
||||
}
|
||||
this.setStartComment(comment);
|
||||
this.mostRecentEvent = e;
|
||||
}
|
||||
|
||||
/* Begin functions defining what actions to take to execute clicks on each
|
||||
* type of target. Any developer wanting to add behaviour on clicks should
|
||||
* modify only this code. */
|
||||
|
||||
/** Execute a bubble click. */
|
||||
private doBubbleClick() {
|
||||
// TODO (#1673): Consistent handling of single clicks.
|
||||
if (this.startBubble instanceof WorkspaceCommentSvg) {
|
||||
this.startBubble.setFocus();
|
||||
this.startBubble.select();
|
||||
}
|
||||
}
|
||||
|
||||
/** Execute a field click. */
|
||||
private doFieldClick() {
|
||||
if (!this.startField) {
|
||||
@@ -963,7 +891,8 @@ export class Gesture {
|
||||
eventUtils.setGroup(true);
|
||||
}
|
||||
const newBlock = this.flyout.createBlock(this.targetBlock);
|
||||
newBlock.scheduleSnapAndBump();
|
||||
newBlock.snapToGrid();
|
||||
newBlock.bumpNeighbours();
|
||||
}
|
||||
} else {
|
||||
if (!this.startWorkspace_) {
|
||||
@@ -1063,6 +992,18 @@ export class Gesture {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the comment that a gesture started on
|
||||
*
|
||||
* @param comment The comment the gesture started on.
|
||||
* @internal
|
||||
*/
|
||||
setStartComment(comment: RenderedWorkspaceComment) {
|
||||
if (!this.startComment) {
|
||||
this.startComment = comment;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the block that a gesture started on, and set the target block
|
||||
* appropriately.
|
||||
@@ -1074,6 +1015,7 @@ export class Gesture {
|
||||
// If the gesture already went through a bubble, don't set the start block.
|
||||
if (!this.startBlock && !this.startBubble) {
|
||||
this.startBlock = block;
|
||||
common.setSelected(this.startBlock);
|
||||
if (block.isInFlyout && block !== block.getRootBlock()) {
|
||||
this.setTargetBlock(block.getRootBlock());
|
||||
} else {
|
||||
@@ -1138,6 +1080,10 @@ export class Gesture {
|
||||
return hasStartBubble && !this.hasExceededDragRadius;
|
||||
}
|
||||
|
||||
private isCommentClick(): boolean {
|
||||
return !!this.startComment && !this.hasExceededDragRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this gesture is a click on a block. This should only be called
|
||||
* when ending a gesture (pointerup).
|
||||
@@ -1196,6 +1142,11 @@ export class Gesture {
|
||||
|
||||
/* End helper functions defining types of clicks. */
|
||||
|
||||
/** Returns the current dragger if the gesture is a drag. */
|
||||
getCurrentDragger(): WorkspaceDragger | IDragger | null {
|
||||
return this.workspaceDragger ?? this.dragger ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this gesture is a drag of either a workspace or block.
|
||||
* This function is called externally to block actions that cannot be taken
|
||||
@@ -1205,9 +1156,7 @@ export class Gesture {
|
||||
* @internal
|
||||
*/
|
||||
isDragging(): boolean {
|
||||
return (
|
||||
!!this.workspaceDragger || !!this.blockDragger || !!this.bubbleDragger
|
||||
);
|
||||
return this.dragging;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1222,31 +1171,6 @@ export class Gesture {
|
||||
return this.gestureHasStarted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of the insertion markers that currently exist. Block drags have
|
||||
* 0, 1, or 2 insertion markers.
|
||||
*
|
||||
* @returns A possibly empty list of insertion marker blocks.
|
||||
* @internal
|
||||
*/
|
||||
getInsertionMarkers(): BlockSvg[] {
|
||||
if (this.blockDragger) {
|
||||
return this.blockDragger.getInsertionMarkers();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current dragger if an item is being dragged. Null if nothing is
|
||||
* being dragged.
|
||||
*
|
||||
* @returns The dragger that is currently in use or null if no drag is in
|
||||
* progress.
|
||||
*/
|
||||
getCurrentDragger(): WorkspaceDragger | BubbleDragger | IBlockDragger | null {
|
||||
return this.blockDragger ?? this.workspaceDragger ?? this.bubbleDragger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a drag or other gesture currently in progress on any workspace?
|
||||
*
|
||||
|
||||
20
core/grid.ts
20
core/grid.ts
@@ -13,6 +13,7 @@
|
||||
// Former goog.module ID: Blockly.Grid
|
||||
|
||||
import * as dom from './utils/dom.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import {Svg} from './utils/svg.js';
|
||||
import {GridOptions} from './options.js';
|
||||
|
||||
@@ -184,6 +185,25 @@ export class Grid {
|
||||
this.pattern.setAttribute('y', `${y}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a coordinate, return the nearest coordinate aligned to the grid.
|
||||
*
|
||||
* @param xy A workspace coordinate.
|
||||
* @returns Workspace coordinate of nearest grid point.
|
||||
* If there's no change, return the same coordinate object.
|
||||
*/
|
||||
alignXY(xy: Coordinate): Coordinate {
|
||||
const spacing = this.getSpacing();
|
||||
const half = spacing / 2;
|
||||
const x = Math.round(Math.round((xy.x - half) / spacing) * spacing + half);
|
||||
const y = Math.round(Math.round((xy.y - half) / spacing) * spacing + half);
|
||||
if (x === xy.x && y === xy.y) {
|
||||
// No change.
|
||||
return xy;
|
||||
}
|
||||
return new Coordinate(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the DOM for the grid described by options.
|
||||
*
|
||||
|
||||
@@ -22,6 +22,7 @@ import {Svg} from '../utils/svg.js';
|
||||
import {TextBubble} from '../bubbles/text_bubble.js';
|
||||
import {TextInputBubble} from '../bubbles/textinput_bubble.js';
|
||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import * as renderManagement from '../render_management.js';
|
||||
|
||||
/** The size of the comment icon in workspace-scale units. */
|
||||
const SIZE = 17;
|
||||
@@ -138,12 +139,12 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
||||
* Updates the state of the bubble (editable / noneditable) to reflect the
|
||||
* state of the bubble if the bubble is currently shown.
|
||||
*/
|
||||
override updateEditable(): void {
|
||||
override async updateEditable(): Promise<void> {
|
||||
super.updateEditable();
|
||||
if (this.bubbleIsVisible()) {
|
||||
// Close and reopen the bubble to display the correct UI.
|
||||
this.setBubbleVisible(false);
|
||||
this.setBubbleVisible(true);
|
||||
await this.setBubbleVisible(false);
|
||||
await this.setBubbleVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,8 +215,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
||||
state['height'] ?? DEFAULT_BUBBLE_HEIGHT,
|
||||
);
|
||||
this.bubbleVisiblity = state['pinned'] ?? false;
|
||||
// Give the block a chance to be positioned and rendered before showing.
|
||||
setTimeout(() => this.setBubbleVisible(this.bubbleVisiblity), 1);
|
||||
this.setBubbleVisible(this.bubbleVisiblity);
|
||||
}
|
||||
|
||||
override onClick(): void {
|
||||
@@ -263,13 +263,18 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
||||
return this.bubbleVisiblity;
|
||||
}
|
||||
|
||||
setBubbleVisible(visible: boolean): void {
|
||||
if (visible && (this.textBubble || this.textInputBubble)) return;
|
||||
if (!visible && !(this.textBubble || this.textInputBubble)) return;
|
||||
|
||||
async setBubbleVisible(visible: boolean): Promise<void> {
|
||||
if (this.bubbleVisiblity === visible) return;
|
||||
this.bubbleVisiblity = visible;
|
||||
|
||||
if (!this.sourceBlock.rendered || this.sourceBlock.isInFlyout) return;
|
||||
await renderManagement.finishQueuedRenders();
|
||||
|
||||
if (
|
||||
!this.sourceBlock.rendered ||
|
||||
this.sourceBlock.isInFlyout ||
|
||||
this.sourceBlock.isInsertionMarker()
|
||||
)
|
||||
return;
|
||||
|
||||
if (visible) {
|
||||
if (this.sourceBlock.isEditable()) {
|
||||
@@ -347,10 +352,18 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
||||
}
|
||||
}
|
||||
|
||||
/** The save state format for a comment icon. */
|
||||
export interface CommentState {
|
||||
/** The text of the comment. */
|
||||
text?: string;
|
||||
|
||||
/** True if the comment is open, false otherwise. */
|
||||
pinned?: boolean;
|
||||
|
||||
/** The height of the comment bubble. */
|
||||
height?: number;
|
||||
|
||||
/** The width of the comment bubble. */
|
||||
width?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import * as dom from '../utils/dom.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import type {IconType} from './icon_types.js';
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as tooltip from '../tooltip.js';
|
||||
|
||||
/**
|
||||
@@ -145,14 +144,4 @@ export abstract class Icon implements IIcon {
|
||||
isClickableInFlyout(autoClosingFlyout: boolean): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of the icon's bubble if one exists.
|
||||
*
|
||||
* @deprecated Use `setBubbleVisible` instead. To be removed in v11.
|
||||
*/
|
||||
setVisible(visibility: boolean): void {
|
||||
deprecation.warn('setVisible', 'v10', 'v11', 'setBubbleVisible');
|
||||
if (hasBubble(this)) this.setBubbleVisible(visibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {ICommentIcon} from '../interfaces/i_comment_icon.js';
|
||||
import {IIcon} from '../interfaces/i_icon.js';
|
||||
import {CommentIcon} from './comment_icon.js';
|
||||
import {MutatorIcon} from './mutator_icon.js';
|
||||
import {WarningIcon} from './warning_icon.js';
|
||||
|
||||
@@ -28,5 +28,5 @@ export class IconType<_T extends IIcon> {
|
||||
|
||||
static MUTATOR = new IconType<MutatorIcon>('mutator');
|
||||
static WARNING = new IconType<WarningIcon>('warning');
|
||||
static COMMENT = new IconType<CommentIcon>('comment');
|
||||
static COMMENT = new IconType<ICommentIcon>('comment');
|
||||
}
|
||||
|
||||
@@ -7,11 +7,9 @@
|
||||
// Former goog.module ID: Blockly.Mutator
|
||||
|
||||
import type {Abstract} from '../events/events_abstract.js';
|
||||
import type {Block} from '../block.js';
|
||||
import {BlockChange} from '../events/events_block_change.js';
|
||||
import type {BlocklyOptions} from '../blockly_options.js';
|
||||
import type {BlockSvg} from '../block_svg.js';
|
||||
import type {Connection} from '../connection.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import * as dom from '../utils/dom.js';
|
||||
import * as eventUtils from '../events/utils.js';
|
||||
@@ -22,8 +20,8 @@ import {Rect} from '../utils/rect.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import {IconType} from './icon_types.js';
|
||||
import * as renderManagement from '../render_management.js';
|
||||
|
||||
/** The size of the mutator icon in workspace-scale units. */
|
||||
const SIZE = 17;
|
||||
@@ -165,9 +163,11 @@ export class MutatorIcon extends Icon implements IHasBubble {
|
||||
return !!this.miniWorkspaceBubble;
|
||||
}
|
||||
|
||||
setBubbleVisible(visible: boolean): void {
|
||||
async setBubbleVisible(visible: boolean): Promise<void> {
|
||||
if (this.bubbleIsVisible() === visible) return;
|
||||
|
||||
await renderManagement.finishQueuedRenders();
|
||||
|
||||
if (visible) {
|
||||
this.miniWorkspaceBubble = new MiniWorkspaceBubble(
|
||||
this.getMiniWorkspaceConfig(),
|
||||
@@ -351,40 +351,4 @@ export class MutatorIcon extends Icon implements IHasBubble {
|
||||
getWorkspace(): WorkspaceSvg | undefined {
|
||||
return this.miniWorkspaceBubble?.getWorkspace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnects the given connection to the mutated input on the given block.
|
||||
*
|
||||
* @deprecated Use connection.reconnect instead. To be removed in v11.
|
||||
*/
|
||||
static reconnect(
|
||||
connectionChild: Connection | null,
|
||||
block: Block,
|
||||
inputName: string,
|
||||
): boolean {
|
||||
deprecation.warn(
|
||||
'MutatorIcon.reconnect',
|
||||
'v10',
|
||||
'v11',
|
||||
'connection.reconnect',
|
||||
);
|
||||
if (!connectionChild) return false;
|
||||
return connectionChild.reconnect(block, inputName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent workspace of a workspace that is inside a mini workspace
|
||||
* bubble, taking into account whether the workspace is a flyout.
|
||||
*
|
||||
* @deprecated Use workspace.getRootWorkspace. To be removed in v11.
|
||||
*/
|
||||
static findParentWs(workspace: WorkspaceSvg): WorkspaceSvg | null {
|
||||
deprecation.warn(
|
||||
'MutatorIcon.findParentWs',
|
||||
'v10',
|
||||
'v11',
|
||||
'workspace.getRootWorkspace',
|
||||
);
|
||||
return workspace.getRootWorkspace();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {Size} from '../utils.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import {TextBubble} from '../bubbles/text_bubble.js';
|
||||
import {IconType} from './icon_types.js';
|
||||
import * as renderManagement from '../render_management.js';
|
||||
|
||||
/** The size of the warning icon in workspace-scale units. */
|
||||
const SIZE = 17;
|
||||
@@ -168,9 +169,11 @@ export class WarningIcon extends Icon implements IHasBubble {
|
||||
return !!this.textBubble;
|
||||
}
|
||||
|
||||
setBubbleVisible(visible: boolean): void {
|
||||
async setBubbleVisible(visible: boolean): Promise<void> {
|
||||
if (this.bubbleIsVisible() === visible) return;
|
||||
|
||||
await renderManagement.finishQueuedRenders();
|
||||
|
||||
if (visible) {
|
||||
this.textBubble = new TextBubble(
|
||||
this.getText(),
|
||||
|
||||
@@ -53,7 +53,10 @@ export function inject(
|
||||
}
|
||||
const options = new Options(opt_options || ({} as BlocklyOptions));
|
||||
const subContainer = document.createElement('div');
|
||||
subContainer.className = 'injectionDiv';
|
||||
dom.addClass(subContainer, 'injectionDiv');
|
||||
if (opt_options?.rtl) {
|
||||
dom.addClass(subContainer, 'blocklyRTL');
|
||||
}
|
||||
subContainer.tabIndex = 0;
|
||||
aria.setState(subContainer, aria.State.LABEL, Msg['WORKSPACE_ARIA_LABEL']);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {Field} from '../field.js';
|
||||
import * as fieldRegistry from '../field_registry.js';
|
||||
import type {RenderedConnection} from '../rendered_connection.js';
|
||||
import {inputTypes} from './input_types.js';
|
||||
import {Align} from './align.js';
|
||||
|
||||
/** Class for an input with optional fields. */
|
||||
export class Input {
|
||||
@@ -102,10 +103,7 @@ export class Input {
|
||||
}
|
||||
|
||||
field.setSourceBlock(this.sourceBlock);
|
||||
if (this.sourceBlock.rendered) {
|
||||
field.init();
|
||||
field.applyColour();
|
||||
}
|
||||
if (this.sourceBlock.initialized) this.initField(field);
|
||||
field.name = opt_name;
|
||||
field.setVisible(this.isVisible());
|
||||
|
||||
@@ -123,8 +121,6 @@ export class Input {
|
||||
|
||||
if (this.sourceBlock.rendered) {
|
||||
(this.sourceBlock as BlockSvg).queueRender();
|
||||
// Adding a field will cause the block to change shape.
|
||||
this.sourceBlock.bumpNeighbours();
|
||||
}
|
||||
return index;
|
||||
}
|
||||
@@ -145,8 +141,6 @@ export class Input {
|
||||
this.fieldRow.splice(i, 1);
|
||||
if (this.sourceBlock.rendered) {
|
||||
(this.sourceBlock as BlockSvg).queueRender();
|
||||
// Removing a field will cause the block to change shape.
|
||||
this.sourceBlock.bumpNeighbours();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -273,11 +267,28 @@ export class Input {
|
||||
|
||||
/** Initialize the fields on this input. */
|
||||
init() {
|
||||
if (!this.sourceBlock.workspace.rendered) {
|
||||
return; // Headless blocks don't need fields initialized.
|
||||
for (const field of this.fieldRow) {
|
||||
field.init();
|
||||
}
|
||||
for (let i = 0; i < this.fieldRow.length; i++) {
|
||||
this.fieldRow[i].init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the fields on this input for a headless block.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public initModel() {
|
||||
for (const field of this.fieldRow) {
|
||||
field.initModel();
|
||||
}
|
||||
}
|
||||
|
||||
/** Initializes the given field. */
|
||||
private initField(field: Field) {
|
||||
if (this.sourceBlock.rendered) {
|
||||
field.init();
|
||||
} else {
|
||||
field.initModel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,25 +316,3 @@ export class Input {
|
||||
return this.sourceBlock.makeConnection_(type);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Input {
|
||||
// TODO(v11): When this is removed in v11, also re-enable errors on access
|
||||
// of deprecated things (in build_tasks.js).
|
||||
/**
|
||||
* Enum for alignment of inputs.
|
||||
*
|
||||
* @deprecated Use Blockly.inputs.Align. To be removed in v11.
|
||||
*/
|
||||
export enum Align {
|
||||
LEFT = -1,
|
||||
CENTRE = 0,
|
||||
RIGHT = 1,
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use Blockly.inputs.Align. To be removed in v11. */
|
||||
/** @suppress {deprecated} */
|
||||
export type Align = Input.Align;
|
||||
/** @deprecated Use Blockly.inputs.Align. To be removed in v11. */
|
||||
/** @suppress {deprecated} */
|
||||
export const Align = Input.Align;
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {BlockSvg} from './block_svg.js';
|
||||
import * as common from './common.js';
|
||||
import {ComponentManager} from './component_manager.js';
|
||||
import {config} from './config.js';
|
||||
import * as constants from './constants.js';
|
||||
import * as blocks from './serialization/blocks.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import type {IDeleteArea} from './interfaces/i_delete_area.js';
|
||||
import type {IDragTarget} from './interfaces/i_drag_target.js';
|
||||
@@ -42,16 +42,6 @@ interface CandidateConnection {
|
||||
radius: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* An error message to throw if the block created by createMarkerBlock_ is
|
||||
* missing any components.
|
||||
*/
|
||||
const DUPLICATE_BLOCK_ERROR =
|
||||
'The insertion marker ' +
|
||||
'manager tried to create a marker but the result is missing %1. If ' +
|
||||
'you are using a mutator, make sure your domToMutation method is ' +
|
||||
'properly defined.';
|
||||
|
||||
/**
|
||||
* Class that controls updates to connections during drags. It is primarily
|
||||
* responsible for finding the closest eligible connection and highlighting or
|
||||
@@ -188,18 +178,16 @@ export class InsertionMarkerManager {
|
||||
eventUtils.enable();
|
||||
const {local, closest} = this.activeCandidate;
|
||||
local.connect(closest);
|
||||
if (this.topBlock.rendered) {
|
||||
const inferiorConnection = local.isSuperior() ? closest : local;
|
||||
const rootBlock = this.topBlock.getRootBlock();
|
||||
const inferiorConnection = local.isSuperior() ? closest : local;
|
||||
const rootBlock = this.topBlock.getRootBlock();
|
||||
|
||||
finishQueuedRenders().then(() => {
|
||||
blockAnimations.connectionUiEffect(inferiorConnection.getSourceBlock());
|
||||
// bringToFront is incredibly expensive. Delay until the next frame.
|
||||
setTimeout(() => {
|
||||
rootBlock.bringToFront();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
finishQueuedRenders().then(() => {
|
||||
blockAnimations.connectionUiEffect(inferiorConnection.getSourceBlock());
|
||||
// bringToFront is incredibly expensive. Delay until the next frame.
|
||||
setTimeout(() => {
|
||||
rootBlock.bringToFront();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,53 +221,30 @@ export class InsertionMarkerManager {
|
||||
* @returns The insertion marker that represents the given block.
|
||||
*/
|
||||
private createMarkerBlock(sourceBlock: BlockSvg): BlockSvg {
|
||||
const imType = sourceBlock.type;
|
||||
|
||||
eventUtils.disable();
|
||||
let result: BlockSvg;
|
||||
try {
|
||||
result = this.workspace.newBlock(imType);
|
||||
result.setInsertionMarker(true);
|
||||
if (sourceBlock.saveExtraState) {
|
||||
const state = sourceBlock.saveExtraState(true);
|
||||
if (state && result.loadExtraState) {
|
||||
result.loadExtraState(state);
|
||||
}
|
||||
} else if (sourceBlock.mutationToDom) {
|
||||
const oldMutationDom = sourceBlock.mutationToDom();
|
||||
if (oldMutationDom && result.domToMutation) {
|
||||
result.domToMutation(oldMutationDom);
|
||||
}
|
||||
}
|
||||
// Copy field values from the other block. These values may impact the
|
||||
// rendered size of the insertion marker. Note that we do not care about
|
||||
// child blocks here.
|
||||
for (let i = 0; i < sourceBlock.inputList.length; i++) {
|
||||
const sourceInput = sourceBlock.inputList[i];
|
||||
if (sourceInput.name === constants.COLLAPSED_INPUT_NAME) {
|
||||
continue; // Ignore the collapsed input.
|
||||
}
|
||||
const resultInput = result.inputList[i];
|
||||
if (!resultInput) {
|
||||
throw new Error(DUPLICATE_BLOCK_ERROR.replace('%1', 'an input'));
|
||||
}
|
||||
for (let j = 0; j < sourceInput.fieldRow.length; j++) {
|
||||
const sourceField = sourceInput.fieldRow[j];
|
||||
const resultField = resultInput.fieldRow[j];
|
||||
if (!resultField) {
|
||||
throw new Error(DUPLICATE_BLOCK_ERROR.replace('%1', 'a field'));
|
||||
}
|
||||
resultField.setValue(sourceField.getValue());
|
||||
}
|
||||
const blockJson = blocks.save(sourceBlock, {
|
||||
addCoordinates: false,
|
||||
addInputBlocks: false,
|
||||
addNextBlocks: false,
|
||||
doFullSerialization: false,
|
||||
});
|
||||
|
||||
if (!blockJson) {
|
||||
throw new Error(
|
||||
`Failed to serialize source block. ${sourceBlock.toDevString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
result = blocks.append(blockJson, this.workspace) as BlockSvg;
|
||||
|
||||
// Turn shadow blocks that are created programmatically during
|
||||
// initalization to insertion markers too.
|
||||
for (const block of result.getDescendants(false)) {
|
||||
block.setInsertionMarker(true);
|
||||
}
|
||||
|
||||
result.setCollapsed(sourceBlock.isCollapsed());
|
||||
result.setInputsInline(sourceBlock.getInputsInline());
|
||||
|
||||
result.initSvg();
|
||||
result.getSvgRoot().setAttribute('visibility', 'hidden');
|
||||
} finally {
|
||||
@@ -419,10 +384,7 @@ export class InsertionMarkerManager {
|
||||
ComponentManager.Capability.DELETE_AREA,
|
||||
);
|
||||
if (isDeleteArea) {
|
||||
return (dragTarget as IDeleteArea).wouldDelete(
|
||||
this.topBlock,
|
||||
newCandidate,
|
||||
);
|
||||
return (dragTarget as IDeleteArea).wouldDelete(this.topBlock);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -8,8 +8,8 @@ import {BlockSvg} from './block_svg.js';
|
||||
import {IConnectionPreviewer} from './interfaces/i_connection_previewer.js';
|
||||
import {RenderedConnection} from './rendered_connection.js';
|
||||
import {WorkspaceSvg} from './workspace_svg.js';
|
||||
import * as blocks from './serialization/blocks.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import * as constants from './constants.js';
|
||||
import * as renderManagement from './render_management.js';
|
||||
import * as registry from './registry.js';
|
||||
import {Renderer as ZelosRenderer} from './renderers/zelos/renderer.js';
|
||||
@@ -21,8 +21,8 @@ import {ConnectionType} from './connection_type.js';
|
||||
*/
|
||||
const DUPLICATE_BLOCK_ERROR =
|
||||
'The insertion marker previewer tried to create a marker but the result ' +
|
||||
'is missing %1. If you are using a mutator, make sure your domToMutation ' +
|
||||
'method is properly defined.';
|
||||
'is missing a connection. If you are using a mutator, make sure your ' +
|
||||
'domToMutation method is properly defined.';
|
||||
|
||||
export class InsertionMarkerPreviewer implements IConnectionPreviewer {
|
||||
private readonly workspace: WorkspaceSvg;
|
||||
@@ -88,6 +88,16 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer {
|
||||
eventUtils.disable();
|
||||
try {
|
||||
this.hidePreview();
|
||||
const dragged = draggedConn.getSourceBlock();
|
||||
const marker = this.createInsertionMarker(dragged);
|
||||
const markerConn = this.getMatchingConnection(
|
||||
dragged,
|
||||
marker,
|
||||
draggedConn,
|
||||
);
|
||||
if (!markerConn) {
|
||||
throw Error(DUPLICATE_BLOCK_ERROR);
|
||||
}
|
||||
|
||||
// TODO(7898): Instead of special casing, we should change the dragger to
|
||||
// track the change in distance between the dragged connection and the
|
||||
@@ -144,60 +154,44 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer {
|
||||
};
|
||||
const originalOffsetInBlock = markerConn.getOffsetInBlock().clone();
|
||||
renderManagement.finishQueuedRenders().then(() => {
|
||||
// Position so that the existing block doesn't move.
|
||||
marker?.positionNearConnection(
|
||||
markerConn,
|
||||
originalOffsetToTarget,
|
||||
originalOffsetInBlock,
|
||||
);
|
||||
marker?.getSvgRoot().setAttribute('visibility', 'visible');
|
||||
eventUtils.disable();
|
||||
try {
|
||||
// Position so that the existing block doesn't move.
|
||||
marker?.positionNearConnection(
|
||||
markerConn,
|
||||
originalOffsetToTarget,
|
||||
originalOffsetInBlock,
|
||||
);
|
||||
marker?.getSvgRoot().setAttribute('visibility', 'visible');
|
||||
} finally {
|
||||
eventUtils.enable();
|
||||
}
|
||||
});
|
||||
return markerConn;
|
||||
}
|
||||
|
||||
private createInsertionMarker(origBlock: BlockSvg) {
|
||||
const result = this.workspace.newBlock(origBlock.type);
|
||||
result.setInsertionMarker(true);
|
||||
if (origBlock.saveExtraState) {
|
||||
const state = origBlock.saveExtraState(true);
|
||||
if (state && result.loadExtraState) {
|
||||
result.loadExtraState(state);
|
||||
}
|
||||
} else if (origBlock.mutationToDom) {
|
||||
const oldMutationDom = origBlock.mutationToDom();
|
||||
if (oldMutationDom && result.domToMutation) {
|
||||
result.domToMutation(oldMutationDom);
|
||||
}
|
||||
}
|
||||
// Copy field values from the other block. These values may impact the
|
||||
// rendered size of the insertion marker. Note that we do not care about
|
||||
// child blocks here.
|
||||
for (let i = 0; i < origBlock.inputList.length; i++) {
|
||||
const sourceInput = origBlock.inputList[i];
|
||||
if (sourceInput.name === constants.COLLAPSED_INPUT_NAME) {
|
||||
continue; // Ignore the collapsed input.
|
||||
}
|
||||
const resultInput = result.inputList[i];
|
||||
if (!resultInput) {
|
||||
throw new Error(DUPLICATE_BLOCK_ERROR.replace('%1', 'an input'));
|
||||
}
|
||||
for (let j = 0; j < sourceInput.fieldRow.length; j++) {
|
||||
const sourceField = sourceInput.fieldRow[j];
|
||||
const resultField = resultInput.fieldRow[j];
|
||||
if (!resultField) {
|
||||
throw new Error(DUPLICATE_BLOCK_ERROR.replace('%1', 'a field'));
|
||||
}
|
||||
resultField.setValue(sourceField.getValue());
|
||||
}
|
||||
const blockJson = blocks.save(origBlock, {
|
||||
addCoordinates: false,
|
||||
addInputBlocks: false,
|
||||
addNextBlocks: false,
|
||||
doFullSerialization: false,
|
||||
});
|
||||
|
||||
if (!blockJson) {
|
||||
throw new Error(
|
||||
`Failed to serialize source block. ${origBlock.toDevString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = blocks.append(blockJson, this.workspace) as BlockSvg;
|
||||
|
||||
// Turn shadow blocks that are created programmatically during
|
||||
// initalization to insertion markers too.
|
||||
for (const block of result.getDescendants(false)) {
|
||||
block.setInsertionMarker(true);
|
||||
}
|
||||
|
||||
result.setCollapsed(origBlock.isCollapsed());
|
||||
result.setInputsInline(origBlock.getInputsInline());
|
||||
|
||||
result.initSvg();
|
||||
result.getSvgRoot().setAttribute('visibility', 'hidden');
|
||||
return result;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Coordinate} from '../utils/coordinate.js';
|
||||
import type {BlockSvg} from '../block_svg.js';
|
||||
// Former goog.module ID: Blockly.IBlockDragger
|
||||
|
||||
/**
|
||||
* A block dragger interface.
|
||||
*/
|
||||
export interface IBlockDragger {
|
||||
/**
|
||||
* Start dragging a block.
|
||||
*
|
||||
* @param currentDragDeltaXY How far the pointer has moved from the position
|
||||
* at mouse down, in pixel units.
|
||||
* @param healStack Whether or not to heal the stack after disconnecting.
|
||||
*/
|
||||
startDrag(currentDragDeltaXY: Coordinate, healStack: boolean): void;
|
||||
|
||||
/**
|
||||
* Execute a step of block dragging, based on the given event. Update the
|
||||
* display accordingly.
|
||||
*
|
||||
* @param e The most recent move event.
|
||||
* @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): void;
|
||||
|
||||
/**
|
||||
* Finish a block drag and put the block back on the workspace.
|
||||
*
|
||||
* @param e The mouseup/touchend 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): void;
|
||||
|
||||
/**
|
||||
* Get a list of the insertion markers that currently exist. Drags have 0, 1,
|
||||
* or 2 insertion markers.
|
||||
*
|
||||
* @returns A possibly empty list of insertion marker blocks.
|
||||
*/
|
||||
getInsertionMarkers(): BlockSvg[];
|
||||
|
||||
/** Sever all links from this object and do any necessary cleanup. */
|
||||
dispose(): void;
|
||||
}
|
||||
40
core/interfaces/i_comment_icon.ts
Normal file
40
core/interfaces/i_comment_icon.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {IconType} from '../icons/icon_types.js';
|
||||
import {CommentState} from '../icons/comment_icon.js';
|
||||
import {IIcon, isIcon} from './i_icon.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import {IHasBubble, hasBubble} from './i_has_bubble.js';
|
||||
import {ISerializable, isSerializable} from './i_serializable.js';
|
||||
|
||||
export interface ICommentIcon extends IIcon, IHasBubble, ISerializable {
|
||||
setText(text: string): void;
|
||||
|
||||
getText(): string;
|
||||
|
||||
setBubbleSize(size: Size): void;
|
||||
|
||||
getBubbleSize(): Size;
|
||||
|
||||
saveState(): CommentState;
|
||||
|
||||
loadState(state: CommentState): void;
|
||||
}
|
||||
|
||||
/** Checks whether the given object is an ICommentIcon. */
|
||||
export function isCommentIcon(obj: Object): obj is ICommentIcon {
|
||||
return (
|
||||
isIcon(obj) &&
|
||||
hasBubble(obj) &&
|
||||
isSerializable(obj) &&
|
||||
(obj as any)['setText'] !== undefined &&
|
||||
(obj as any)['getText'] !== undefined &&
|
||||
(obj as any)['setBubbleSize'] !== undefined &&
|
||||
(obj as any)['getBubbleSize'] !== undefined &&
|
||||
obj.getType() === IconType.COMMENT
|
||||
);
|
||||
}
|
||||
@@ -16,4 +16,19 @@ export interface IDeletable {
|
||||
* @returns True if deletable.
|
||||
*/
|
||||
isDeletable(): boolean;
|
||||
|
||||
/** Disposes of this object, cleaning up any references or DOM elements. */
|
||||
dispose(): void;
|
||||
|
||||
/** Visually indicates that the object is pending deletion. */
|
||||
setDeleteStyle(wouldDelete: boolean): void;
|
||||
}
|
||||
|
||||
/** Returns whether the given object is an IDeletable. */
|
||||
export function isDeletable(obj: any): obj is IDeletable {
|
||||
return (
|
||||
obj['isDeletable'] !== undefined &&
|
||||
obj['dispose'] !== undefined &&
|
||||
obj['setDeleteStyle'] !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,9 +21,8 @@ export interface IDeleteArea extends IDragTarget {
|
||||
* before onDragEnter/onDragOver/onDragExit.
|
||||
*
|
||||
* @param element The block or bubble currently being dragged.
|
||||
* @param couldConnect Whether the element could could connect to another.
|
||||
* @returns Whether the element provided would be deleted if dropped on this
|
||||
* area.
|
||||
*/
|
||||
wouldDelete(element: IDraggable, couldConnect: boolean): boolean;
|
||||
wouldDelete(element: IDraggable): boolean;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,69 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Former goog.module ID: Blockly.IDraggable
|
||||
|
||||
import type {IDeletable} from './i_deletable.js';
|
||||
import {Coordinate} from '../utils/coordinate';
|
||||
|
||||
/**
|
||||
* The interface for an object that can be dragged.
|
||||
* Represents an object that can be dragged.
|
||||
*/
|
||||
export interface IDraggable extends IDeletable {}
|
||||
export interface IDraggable extends IDragStrategy {
|
||||
/**
|
||||
* Returns the current location of the draggable in workspace
|
||||
* coordinates.
|
||||
*
|
||||
* @returns Coordinate of current location on workspace.
|
||||
*/
|
||||
getRelativeToSurfaceXY(): Coordinate;
|
||||
}
|
||||
|
||||
export interface IDragStrategy {
|
||||
/** Returns true iff the element is currently movable. */
|
||||
isMovable(): boolean;
|
||||
|
||||
/**
|
||||
* Handles any drag startup (e.g moving elements to the front of the
|
||||
* workspace).
|
||||
*
|
||||
* @param e PointerEvent that started the drag; can be used to
|
||||
* check modifier keys, etc. May be missing when dragging is
|
||||
* triggered programatically rather than by user.
|
||||
*/
|
||||
startDrag(e?: PointerEvent): void;
|
||||
|
||||
/**
|
||||
* Handles moving elements to the new location, and updating any
|
||||
* visuals based on that (e.g connection previews for blocks).
|
||||
*
|
||||
* @param newLoc Workspace coordinate to which the draggable has
|
||||
* been dragged.
|
||||
* @param e PointerEvent that continued the drag. Can be
|
||||
* used to check modifier keys, etc.
|
||||
*/
|
||||
drag(newLoc: Coordinate, e?: PointerEvent): void;
|
||||
|
||||
/**
|
||||
* Handles any drag cleanup, including e.g. connecting or deleting
|
||||
* blocks.
|
||||
*
|
||||
* @param newLoc Workspace coordinate at which the drag finished.
|
||||
* been dragged.
|
||||
* @param e PointerEvent that finished the drag. Can be
|
||||
* used to check modifier keys, etc.
|
||||
*/
|
||||
endDrag(e?: PointerEvent): void;
|
||||
|
||||
/** Moves the draggable back to where it was at the start of the drag. */
|
||||
revertDrag(): void;
|
||||
}
|
||||
|
||||
/** Returns whether the given object is an IDraggable or not. */
|
||||
export function isDraggable(obj: any): obj is IDraggable {
|
||||
return (
|
||||
obj.getRelativeToSurfaceXY !== undefined &&
|
||||
obj.isMovable !== undefined &&
|
||||
obj.startDrag !== undefined &&
|
||||
obj.drag !== undefined &&
|
||||
obj.endDrag !== undefined &&
|
||||
obj.revertDrag !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
35
core/interfaces/i_dragger.ts
Normal file
35
core/interfaces/i_dragger.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {Coordinate} from '../utils/coordinate';
|
||||
|
||||
export interface IDragger {
|
||||
/**
|
||||
* Handles any drag startup.
|
||||
*
|
||||
* @param e PointerEvent that started the drag.
|
||||
*/
|
||||
onDragStart(e: PointerEvent): void;
|
||||
|
||||
/**
|
||||
* Handles dragging, including calculating where the element should
|
||||
* actually be moved to.
|
||||
*
|
||||
* @param e PointerEvent that continued the drag.
|
||||
* @param totalDelta The total distance, in pixels, that the mouse
|
||||
* has moved since the start of the drag.
|
||||
*/
|
||||
onDrag(e: PointerEvent, totalDelta: Coordinate): void;
|
||||
|
||||
/**
|
||||
* Handles any drag cleanup.
|
||||
*
|
||||
* @param e PointerEvent that finished the drag.
|
||||
* @param totalDelta The total distance, in pixels, that the mouse
|
||||
* has moved since the start of the drag.
|
||||
*/
|
||||
onDragEnd(e: PointerEvent, totalDelta: Coordinate): void;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export interface IHasBubble {
|
||||
bubbleIsVisible(): boolean;
|
||||
|
||||
/** Sets whether the bubble is open or not. */
|
||||
setBubbleVisible(visible: boolean): void;
|
||||
setBubbleVisible(visible: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
/** Type guard that checks whether the given object is a IHasBubble. */
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {ParameterState} from '../serialization/procedures';
|
||||
import {IProcedureModel} from './i_procedure_model';
|
||||
|
||||
/**
|
||||
@@ -40,4 +41,11 @@ export interface IParameterModel {
|
||||
|
||||
/** Sets the procedure model this parameter is associated with. */
|
||||
setProcedureModel(model: IProcedureModel): this;
|
||||
|
||||
/**
|
||||
* Serializes the state of the parameter to JSON.
|
||||
*
|
||||
* @returns JSON serializable state of the parameter.
|
||||
*/
|
||||
saveState(): ParameterState;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {State} from '../serialization/procedures.js';
|
||||
import {IParameterModel} from './i_parameter_model.js';
|
||||
|
||||
/**
|
||||
@@ -60,4 +61,11 @@ export interface IProcedureModel {
|
||||
* disabled, all procedure caller blocks should be disabled as well.
|
||||
*/
|
||||
getEnabled(): boolean;
|
||||
|
||||
/**
|
||||
* Serializes the state of the procedure to JSON.
|
||||
*
|
||||
* @returns JSON serializable state of the procedure.
|
||||
*/
|
||||
saveState(): State;
|
||||
}
|
||||
|
||||
@@ -6,18 +6,29 @@
|
||||
|
||||
// Former goog.module ID: Blockly.ISelectable
|
||||
|
||||
import type {IDeletable} from './i_deletable.js';
|
||||
import type {IMovable} from './i_movable.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
/**
|
||||
* The interface for an object that is selectable.
|
||||
*/
|
||||
export interface ISelectable extends IDeletable, IMovable {
|
||||
export interface ISelectable {
|
||||
id: string;
|
||||
|
||||
workspace: Workspace;
|
||||
|
||||
/** Select this. Highlight it visually. */
|
||||
select(): void;
|
||||
|
||||
/** Unselect this. Unhighlight it visually. */
|
||||
unselect(): void;
|
||||
}
|
||||
|
||||
/** Checks whether the given object is an ISelectable. */
|
||||
export function isSelectable(obj: Object): obj is ISelectable {
|
||||
return (
|
||||
typeof (obj as any).id === 'string' &&
|
||||
(obj as any).workspace !== undefined &&
|
||||
(obj as any).select !== undefined &&
|
||||
(obj as any).unselect !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,14 +15,6 @@ import {ConnectionType} from './connection_type.js';
|
||||
*/
|
||||
export const COLLAPSE_CHARS = 30;
|
||||
|
||||
/**
|
||||
* When dragging a block out of a stack, split the stack in two (true), or drag
|
||||
* out the block healing the stack (false).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const DRAG_STACK = true;
|
||||
|
||||
/**
|
||||
* Lookup table for determining the opposite type of a connection.
|
||||
*
|
||||
|
||||
76
core/main.ts
76
core/main.ts
@@ -12,83 +12,7 @@
|
||||
|
||||
// Former goog.module ID: Blockly.main
|
||||
|
||||
import * as Blockly from './blockly.js';
|
||||
import * as Msg from './msg.js';
|
||||
import * as colour from './utils/colour.js';
|
||||
import * as deprecation from './utils/deprecation.js';
|
||||
|
||||
/*
|
||||
* Aliased functions and properties that used to be on the Blockly namespace.
|
||||
* Everything in this section is deprecated. Both external and internal code
|
||||
* should avoid using these functions and use the designated replacements.
|
||||
* Everything in this section will be removed in a future version of Blockly.
|
||||
*/
|
||||
|
||||
// Add accessors for properties on Blockly that have now been deprecated.
|
||||
// This will not work in uncompressed mode; it depends on Blockly being
|
||||
// transpiled from a ES Module object to a plain object by Closure Compiler.
|
||||
Object.defineProperties(Blockly, {
|
||||
/**
|
||||
* The richness of block colours, regardless of the hue.
|
||||
* Must be in the range of 0 (inclusive) to 1 (exclusive).
|
||||
*
|
||||
* @name Blockly.HSV_SATURATION
|
||||
* @type {number}
|
||||
* @deprecated Use Blockly.utils.colour.getHsvSaturation() /
|
||||
* .setHsvSaturation() instead. (July 2023)
|
||||
* @suppress {checkTypes}
|
||||
*/
|
||||
HSV_SATURATION: {
|
||||
get: function () {
|
||||
deprecation.warn(
|
||||
'Blockly.HSV_SATURATION',
|
||||
'version 10',
|
||||
'version 11',
|
||||
'Blockly.utils.colour.getHsvSaturation()',
|
||||
);
|
||||
return colour.getHsvSaturation();
|
||||
},
|
||||
set: function (newValue) {
|
||||
deprecation.warn(
|
||||
'Blockly.HSV_SATURATION',
|
||||
'version 10',
|
||||
'version 11',
|
||||
'Blockly.utils.colour.setHsvSaturation()',
|
||||
);
|
||||
colour.setHsvSaturation(newValue);
|
||||
},
|
||||
},
|
||||
/**
|
||||
* The intensity of block colours, regardless of the hue.
|
||||
* Must be in the range of 0 (inclusive) to 1 (exclusive).
|
||||
*
|
||||
* @name Blockly.HSV_VALUE
|
||||
* @type {number}
|
||||
* @deprecated Use Blockly.utils.colour.getHsvValue() / .setHsvValue instead.
|
||||
* (July 2023)
|
||||
* @suppress {checkTypes}
|
||||
*/
|
||||
HSV_VALUE: {
|
||||
get: function () {
|
||||
deprecation.warn(
|
||||
'Blockly.HSV_VALUE',
|
||||
'version 10',
|
||||
'version 11',
|
||||
'Blockly.utils.colour.getHsvValue()',
|
||||
);
|
||||
return colour.getHsvValue();
|
||||
},
|
||||
set: function (newValue) {
|
||||
deprecation.warn(
|
||||
'Blockly.HSV_VALUE',
|
||||
'version 10',
|
||||
'version 11',
|
||||
'Blockly.utils.colour.setHsvValue()',
|
||||
);
|
||||
colour.setHsvValue(newValue);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// If Blockly is compiled with ADVANCED_COMPILATION and/or loaded as a
|
||||
// CJS or ES module there will not be a Blockly global variable
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// Former goog.module ID: Blockly.Options
|
||||
|
||||
import type {BlocklyOptions} from './blockly_options.js';
|
||||
import * as deprecation from './utils/deprecation.js';
|
||||
import * as registry from './registry.js';
|
||||
import {Theme} from './theme.js';
|
||||
import {Classic} from './theme/classic.js';
|
||||
@@ -38,6 +37,7 @@ export class Options {
|
||||
pathToMedia: string;
|
||||
hasCategories: boolean;
|
||||
moveOptions: MoveOptions;
|
||||
/** @deprecated January 2019 */
|
||||
hasScrollbars: boolean;
|
||||
hasTrashcan: boolean;
|
||||
maxTrashcanContents: number;
|
||||
@@ -143,10 +143,6 @@ export class Options {
|
||||
pathToMedia = options['media'].endsWith('/')
|
||||
? options['media']
|
||||
: options['media'] + '/';
|
||||
} else if ('path' in options) {
|
||||
// 'path' is a deprecated option which has been replaced by 'media'.
|
||||
deprecation.warn('path', 'Nov 2014', 'Jul 2023', 'media');
|
||||
pathToMedia = (options as any)['path'] + 'media/';
|
||||
}
|
||||
const rawOneBasedIndex = options['oneBasedIndex'];
|
||||
const oneBasedIndex =
|
||||
@@ -172,7 +168,6 @@ export class Options {
|
||||
this.pathToMedia = pathToMedia;
|
||||
this.hasCategories = hasCategories;
|
||||
this.moveOptions = Options.parseMoveOptions_(options, hasCategories);
|
||||
/** @deprecated January 2019 */
|
||||
this.hasScrollbars = !!this.moveOptions.scrollbars;
|
||||
this.hasTrashcan = hasTrashcan;
|
||||
this.maxTrashcanContents = maxTrashcanContents;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
import type {Abstract} from './events/events_abstract.js';
|
||||
import type {Field} from './field.js';
|
||||
import type {IBlockDragger} from './interfaces/i_block_dragger.js';
|
||||
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
|
||||
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||
import type {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
||||
@@ -24,6 +23,7 @@ import type {ToolboxItem} from './toolbox/toolbox_item.js';
|
||||
import type {IPaster} from './interfaces/i_paster.js';
|
||||
import type {ICopyData, ICopyable} from './interfaces/i_copyable.js';
|
||||
import type {IConnectionPreviewer} from './interfaces/i_connection_previewer.js';
|
||||
import type {IDragger} from './interfaces/i_dragger.js';
|
||||
|
||||
/**
|
||||
* A map of maps. With the keys being the type and name of the class we are
|
||||
@@ -95,7 +95,11 @@ export class Type<_T> {
|
||||
|
||||
static METRICS_MANAGER = new Type<IMetricsManager>('metricsManager');
|
||||
|
||||
static BLOCK_DRAGGER = new Type<IBlockDragger>('blockDragger');
|
||||
/**
|
||||
* Type for an IDragger. Formerly behavior was mostly covered by
|
||||
* BlockDraggeers, which is why the name is inaccurate.
|
||||
*/
|
||||
static BLOCK_DRAGGER = new Type<IDragger>('blockDragger');
|
||||
|
||||
/** @internal */
|
||||
static SERIALIZER = new Type<ISerializer>('serializer');
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import {BlockSvg} from './block_svg.js';
|
||||
import * as userAgent from './utils/useragent.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
/** The set of all blocks in need of rendering which don't have parents. */
|
||||
@@ -14,6 +15,9 @@ const rootBlocks = new Set<BlockSvg>();
|
||||
/** The set of all blocks in need of rendering. */
|
||||
const dirtyBlocks = new WeakSet<BlockSvg>();
|
||||
|
||||
/** A map from queued blocks to the event group from when they were queued. */
|
||||
const eventGroups = new WeakMap<BlockSvg, string>();
|
||||
|
||||
/**
|
||||
* The promise which resolves after the current set of renders is completed. Or
|
||||
* null if there are no queued renders.
|
||||
@@ -103,6 +107,7 @@ function alwaysImmediatelyRender() {
|
||||
*/
|
||||
function queueBlock(block: BlockSvg) {
|
||||
dirtyBlocks.add(block);
|
||||
eventGroups.set(block, eventUtils.getGroup());
|
||||
const parent = block.getParent();
|
||||
if (parent) {
|
||||
queueBlock(parent);
|
||||
@@ -133,6 +138,15 @@ function doRenders(workspace?: WorkspaceSvg) {
|
||||
const blockOrigin = block.getRelativeToSurfaceXY();
|
||||
block.updateComponentLocations(blockOrigin);
|
||||
}
|
||||
for (const block of blocks) {
|
||||
const oldGroup = eventUtils.getGroup();
|
||||
const newGroup = eventGroups.get(block);
|
||||
if (newGroup) eventUtils.setGroup(newGroup);
|
||||
|
||||
block.bumpNeighbours();
|
||||
|
||||
eventUtils.setGroup(oldGroup);
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
dequeueBlock(block);
|
||||
@@ -144,6 +158,7 @@ function doRenders(workspace?: WorkspaceSvg) {
|
||||
function dequeueBlock(block: BlockSvg) {
|
||||
rootBlocks.delete(block);
|
||||
dirtyBlocks.delete(block);
|
||||
eventGroups.delete(block);
|
||||
for (const child of block.getChildren(false)) {
|
||||
dequeueBlock(child);
|
||||
}
|
||||
@@ -170,6 +185,7 @@ function shouldRenderRootBlock(block: BlockSvg): boolean {
|
||||
*/
|
||||
function renderBlock(block: BlockSvg) {
|
||||
if (!dirtyBlocks.has(block)) return;
|
||||
if (!block.initialized) return;
|
||||
for (const child of block.getChildren(false)) {
|
||||
renderBlock(child);
|
||||
}
|
||||
|
||||
@@ -445,19 +445,20 @@ export class RenderedConnection extends Connection {
|
||||
const {parentConnection, childConnection} =
|
||||
this.getParentAndChildConnections();
|
||||
if (!parentConnection || !childConnection) return;
|
||||
const existingGroup = eventUtils.getGroup();
|
||||
if (!existingGroup) eventUtils.setGroup(true);
|
||||
|
||||
const parent = parentConnection.getSourceBlock() as BlockSvg;
|
||||
const child = childConnection.getSourceBlock() as BlockSvg;
|
||||
super.disconnectInternal(setParent);
|
||||
// Rerender the parent so that it may reflow.
|
||||
if (parent.rendered) {
|
||||
parent.queueRender();
|
||||
}
|
||||
if (child.rendered) {
|
||||
child.updateDisabled();
|
||||
child.queueRender();
|
||||
// Reset visibility, since the child is now a top block.
|
||||
child.getSvgRoot().style.display = 'block';
|
||||
}
|
||||
|
||||
parent.queueRender();
|
||||
child.updateDisabled();
|
||||
child.queueRender();
|
||||
// Reset visibility, since the child is now a top block.
|
||||
child.getSvgRoot().style.display = 'block';
|
||||
|
||||
eventUtils.setGroup(existingGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -500,29 +501,10 @@ export class RenderedConnection extends Connection {
|
||||
|
||||
const parentBlock = this.getSourceBlock();
|
||||
const childBlock = renderedChildConnection.getSourceBlock();
|
||||
const parentRendered = parentBlock.rendered;
|
||||
const childRendered = childBlock.rendered;
|
||||
|
||||
if (parentRendered) {
|
||||
parentBlock.updateDisabled();
|
||||
}
|
||||
if (childRendered) {
|
||||
childBlock.updateDisabled();
|
||||
}
|
||||
if (parentRendered && childRendered) {
|
||||
if (
|
||||
this.type === ConnectionType.NEXT_STATEMENT ||
|
||||
this.type === ConnectionType.PREVIOUS_STATEMENT
|
||||
) {
|
||||
// Child block may need to square off its corners if it is in a stack.
|
||||
// Rendering a child will render its parent.
|
||||
childBlock.queueRender();
|
||||
} else {
|
||||
// Child block does not change shape. Rendering the parent node will
|
||||
// move its connected children into position.
|
||||
parentBlock.queueRender();
|
||||
}
|
||||
}
|
||||
parentBlock.updateDisabled();
|
||||
childBlock.updateDisabled();
|
||||
childBlock.queueRender();
|
||||
|
||||
// The input the child block is connected to (if any).
|
||||
const parentInput = parentBlock.getInputWithBlock(childBlock);
|
||||
@@ -548,8 +530,6 @@ export class RenderedConnection extends Connection {
|
||||
) {
|
||||
const child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
|
||||
child!.unplug();
|
||||
// Bump away.
|
||||
this.sourceBlock_.bumpNeighbours();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,9 +543,7 @@ export class RenderedConnection extends Connection {
|
||||
*/
|
||||
override setCheck(check: string | string[] | null): RenderedConnection {
|
||||
super.setCheck(check);
|
||||
if (this.sourceBlock_.rendered) {
|
||||
this.sourceBlock_.queueRender();
|
||||
}
|
||||
this.sourceBlock_.queueRender();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {Types} from '../measurables/types.js';
|
||||
import {isDynamicShape, isNotch, isPuzzleTab} from './constants.js';
|
||||
import type {ConstantProvider, Notch, PuzzleTab} from './constants.js';
|
||||
import type {RenderInfo} from './info.js';
|
||||
import * as deprecation from '../../utils/deprecation.js';
|
||||
import {ConnectionType} from '../../connection_type.js';
|
||||
|
||||
/**
|
||||
@@ -70,16 +69,6 @@ export class Drawer {
|
||||
this.recordSizeOnBlock_();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide icons that were marked as hidden.
|
||||
*
|
||||
* @deprecated Manually hiding icons is no longer necessary. To be removed
|
||||
* in v11.
|
||||
*/
|
||||
protected hideHiddenIcons_() {
|
||||
deprecation.warn('hideHiddenIcons_', 'v10', 'v11');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save sizing information back to the block
|
||||
* Most of the rendering information can be thrown away at the end of the
|
||||
|
||||
@@ -12,18 +12,12 @@ import type {ConstantProvider} from '../common/constants.js';
|
||||
|
||||
import {Measurable} from './base.js';
|
||||
import {Types} from './types.js';
|
||||
import {hasBubble} from '../../interfaces/i_has_bubble.js';
|
||||
|
||||
/**
|
||||
* An object containing information about the space an icon takes up during
|
||||
* rendering.
|
||||
*/
|
||||
export class Icon extends Measurable {
|
||||
/**
|
||||
* @deprecated Will be removed in v11. Create a subclass of the Icon
|
||||
* measurable if this data is necessary for you.
|
||||
*/
|
||||
isVisible: boolean;
|
||||
flipRtl = false;
|
||||
|
||||
/**
|
||||
@@ -39,7 +33,6 @@ export class Icon extends Measurable {
|
||||
) {
|
||||
super(constants);
|
||||
|
||||
this.isVisible = hasBubble(icon) && icon.bubbleIsVisible();
|
||||
this.type |= Types.ICON;
|
||||
|
||||
const size = icon.getSize();
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Former goog.module ID: Blockly.minimalist.ConstantProvider
|
||||
|
||||
import {ConstantProvider as BaseConstantProvider} from '../common/constants.js';
|
||||
import * as deprecation from '../../utils/deprecation.js';
|
||||
|
||||
/**
|
||||
* An object that provides constants for rendering blocks in the minimalist
|
||||
* renderer.
|
||||
*
|
||||
* @deprecated Use Blockly.blockRendering.ConstantProvider instead.
|
||||
* To be removed in v11.
|
||||
*/
|
||||
export class ConstantProvider extends BaseConstantProvider {
|
||||
/**
|
||||
* @deprecated Use Blockly.blockRendering.ConstantProvider instead.
|
||||
* To be removed in v11.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
deprecation.warn(
|
||||
'Blockly.minimalist.ConstantProvider',
|
||||
'v10',
|
||||
'v11',
|
||||
'Blockly.blockRendering.ConstantProvider',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Former goog.module ID: Blockly.minimalist.Drawer
|
||||
|
||||
import type {BlockSvg} from '../../block_svg.js';
|
||||
import {Drawer as BaseDrawer} from '../common/drawer.js';
|
||||
import * as deprecation from '../../utils/deprecation.js';
|
||||
|
||||
import type {RenderInfo} from './info.js';
|
||||
|
||||
/**
|
||||
* An object that draws a block based on the given rendering information.
|
||||
*
|
||||
* @deprecated Use Blockly.blockRendering.Drawer instead.
|
||||
* To be removed in v11.
|
||||
*/
|
||||
export class Drawer extends BaseDrawer {
|
||||
/**
|
||||
* @param block The block to render.
|
||||
* @param info An object containing all information needed to render this
|
||||
* block.
|
||||
*
|
||||
* @deprecated Use Blockly.blockRendering.Drawer instead.
|
||||
* To be removed in v11.
|
||||
*/
|
||||
constructor(block: BlockSvg, info: RenderInfo) {
|
||||
super(block, info);
|
||||
deprecation.warn(
|
||||
'Blockly.minimalist.Drawer',
|
||||
'v10',
|
||||
'v11',
|
||||
'Blockly.blockRendering.Drawer',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Former goog.module ID: Blockly.minimalist.RenderInfo
|
||||
|
||||
import type {BlockSvg} from '../../block_svg.js';
|
||||
import {RenderInfo as BaseRenderInfo} from '../common/info.js';
|
||||
import * as deprecation from '../../utils/deprecation.js';
|
||||
|
||||
import type {Renderer} from './renderer.js';
|
||||
|
||||
/**
|
||||
* An object containing all sizing information needed to draw this block.
|
||||
*
|
||||
* This measure pass does not propagate changes to the block (although fields
|
||||
* may choose to rerender when getSize() is called). However, calling it
|
||||
* repeatedly may be expensive.
|
||||
*
|
||||
* @deprecated Use Blockly.blockRendering.RenderInfo instead. To be removed
|
||||
* in v11.
|
||||
*/
|
||||
export class RenderInfo extends BaseRenderInfo {
|
||||
// Exclamation is fine b/c this is assigned by the super constructor.
|
||||
protected override renderer_!: Renderer;
|
||||
|
||||
/**
|
||||
* @param renderer The renderer in use.
|
||||
* @param block The block to measure.
|
||||
* @deprecated Use Blockly.blockRendering.RenderInfo instead. To be removed
|
||||
* in v11.
|
||||
*/
|
||||
constructor(renderer: Renderer, block: BlockSvg) {
|
||||
super(renderer, block);
|
||||
deprecation.warn(
|
||||
'Blockly.minimalist.RenderInfo',
|
||||
'v10',
|
||||
'v11',
|
||||
'Blockly.blockRendering.RenderInfo',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block renderer in use.
|
||||
*
|
||||
* @returns The block renderer in use.
|
||||
*/
|
||||
override getRenderer(): Renderer {
|
||||
return this.renderer_;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/** @file Re-exports of Blockly.minimalist.* modules. */
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Former goog.module ID: Blockly.minimalist
|
||||
|
||||
import {ConstantProvider} from './constants.js';
|
||||
import {Drawer} from './drawer.js';
|
||||
import {RenderInfo} from './info.js';
|
||||
import {Renderer} from './renderer.js';
|
||||
|
||||
export {ConstantProvider, Drawer, Renderer, RenderInfo};
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Former goog.module ID: Blockly.minimalist.Renderer
|
||||
|
||||
import type {BlockSvg} from '../../block_svg.js';
|
||||
import * as blockRendering from '../common/block_rendering.js';
|
||||
import type {RenderInfo as BaseRenderInfo} from '../common/info.js';
|
||||
import {Renderer as BaseRenderer} from '../common/renderer.js';
|
||||
import * as deprecation from '../../utils/deprecation.js';
|
||||
|
||||
import {ConstantProvider} from './constants.js';
|
||||
import {Drawer} from './drawer.js';
|
||||
import {RenderInfo} from './info.js';
|
||||
|
||||
/**
|
||||
* The minimalist renderer.
|
||||
*
|
||||
* @deprecated Use Blockly.blockRendering.Renderer instead. To be removed
|
||||
* in v11.
|
||||
*/
|
||||
export class Renderer extends BaseRenderer {
|
||||
/**
|
||||
* @param name The renderer name.
|
||||
* @deprecated Use Blockly.blockRendering.Renderer instead. To be removed
|
||||
* in v11.
|
||||
*/
|
||||
constructor(name: string) {
|
||||
super(name);
|
||||
deprecation.warn(
|
||||
'Blockly.minimalist.Renderer',
|
||||
'v10',
|
||||
'v11',
|
||||
'Blockly.blockRendering.Renderer',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the renderer's constant provider.
|
||||
*
|
||||
* @returns The constant provider.
|
||||
*/
|
||||
protected override makeConstants_(): ConstantProvider {
|
||||
return new ConstantProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the renderer's render info object.
|
||||
*
|
||||
* @param block The block to measure.
|
||||
* @returns The render info object.
|
||||
*/
|
||||
protected override makeRenderInfo_(block: BlockSvg): RenderInfo {
|
||||
return new RenderInfo(this, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the renderer's drawer.
|
||||
*
|
||||
* @param block The block to render.
|
||||
* @param info An object containing all information needed to render this
|
||||
* block.
|
||||
* @returns The drawer.
|
||||
*/
|
||||
protected override makeDrawer_(
|
||||
block: BlockSvg,
|
||||
info: BaseRenderInfo,
|
||||
): Drawer {
|
||||
return new Drawer(block, info as RenderInfo);
|
||||
}
|
||||
}
|
||||
|
||||
blockRendering.register('minimalist', Renderer);
|
||||
@@ -16,6 +16,7 @@ import * as procedures from './serialization/procedures.js';
|
||||
import * as registry from './serialization/registry.js';
|
||||
import * as variables from './serialization/variables.js';
|
||||
import * as workspaces from './serialization/workspaces.js';
|
||||
import * as workspaceComments from './serialization/workspace_comments.js';
|
||||
import {ISerializer} from './interfaces/i_serializer.js';
|
||||
|
||||
export {
|
||||
@@ -26,5 +27,6 @@ export {
|
||||
registry,
|
||||
variables,
|
||||
workspaces,
|
||||
workspaceComments,
|
||||
ISerializer,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import type {Block} from '../block.js';
|
||||
import type {BlockSvg} from '../block_svg.js';
|
||||
import type {Connection} from '../connection.js';
|
||||
import {MANUALLY_DISABLED} from '../constants.js';
|
||||
import * as deprecation from '../utils/deprecation.js';
|
||||
import * as eventUtils from '../events/utils.js';
|
||||
import {inputTypes} from '../inputs/input_types.js';
|
||||
import {isSerializable} from '../interfaces/i_serializable.js';
|
||||
@@ -53,6 +55,7 @@ export interface State {
|
||||
movable?: boolean;
|
||||
editable?: boolean;
|
||||
enabled?: boolean;
|
||||
disabledReasons?: string[];
|
||||
inline?: boolean;
|
||||
data?: string;
|
||||
extraState?: AnyDuringMigration;
|
||||
@@ -158,7 +161,7 @@ function saveAttributes(block: Block, state: State) {
|
||||
state['collapsed'] = true;
|
||||
}
|
||||
if (!block.isEnabled()) {
|
||||
state['enabled'] = false;
|
||||
state['disabledReasons'] = Array.from(block.getDisabledReasons());
|
||||
}
|
||||
if (!block.isOwnDeletable()) {
|
||||
state['deletable'] = false;
|
||||
@@ -520,7 +523,18 @@ function loadAttributes(block: Block, state: State) {
|
||||
block.setEditable(false);
|
||||
}
|
||||
if (state['enabled'] === false) {
|
||||
block.setEnabled(false);
|
||||
deprecation.warn(
|
||||
'enabled',
|
||||
'v11',
|
||||
'v12',
|
||||
'disabledReasons with the value ["' + MANUALLY_DISABLED + '"]',
|
||||
);
|
||||
block.setDisabledReason(true, MANUALLY_DISABLED);
|
||||
}
|
||||
if (Array.isArray(state['disabledReasons'])) {
|
||||
for (const reason of state['disabledReasons']) {
|
||||
block.setDisabledReason(true, reason);
|
||||
}
|
||||
}
|
||||
if (state['inline'] !== undefined) {
|
||||
block.setInputsInline(state['inline']);
|
||||
|
||||
@@ -20,3 +20,6 @@ export const PROCEDURES = 75;
|
||||
* The priority for deserializing blocks.
|
||||
*/
|
||||
export const BLOCKS = 50;
|
||||
|
||||
/** The priority for deserializing workspace comments. */
|
||||
export const WORKSPACE_COMMENTS = 25;
|
||||
|
||||
@@ -10,87 +10,75 @@ import type {ISerializer} from '../interfaces/i_serializer.js';
|
||||
import * as priorities from './priorities.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
|
||||
/**
|
||||
* Representation of a procedure data model.
|
||||
*/
|
||||
/** Represents the state of a procedure model. */
|
||||
export interface State {
|
||||
// TODO: This should also handle enabled.
|
||||
id: string;
|
||||
name: string;
|
||||
returnTypes: string[] | null;
|
||||
parameters?: ParameterState[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of a parameter data model.
|
||||
*/
|
||||
/** Represents the state of a parameter model. */
|
||||
export interface ParameterState {
|
||||
id: string;
|
||||
name: string;
|
||||
types?: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* A newable signature for an IProcedureModel.
|
||||
*
|
||||
* Refer to
|
||||
* https://www.typescriptlang.org/docs/handbook/2/generics.html#using-class-types-in-generics
|
||||
* https://www.typescriptlang.org/docs/handbook/interfaces.html#difference-between-the-static-and-instance-sides-of-classes
|
||||
* for what is going on with this.
|
||||
*/
|
||||
type ProcedureModelConstructor<ProcedureModel extends IProcedureModel> = new (
|
||||
workspace: Workspace,
|
||||
name: string,
|
||||
id: string,
|
||||
) => ProcedureModel;
|
||||
interface ProcedureModelConstructor<ProcedureModel extends IProcedureModel> {
|
||||
new (workspace: Workspace, name: string, id: string): ProcedureModel;
|
||||
|
||||
/**
|
||||
* Deserializes the JSON state and returns a procedure model.
|
||||
*
|
||||
* @param state The state to deserialize.
|
||||
* @param workspace The workspace to load the procedure model into.
|
||||
* @returns The constructed procedure model.
|
||||
*/
|
||||
loadState(state: Object, workspace: Workspace): ProcedureModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* A newable signature for an IParameterModel.
|
||||
*
|
||||
* Refer to
|
||||
* https://www.typescriptlang.org/docs/handbook/2/generics.html#using-class-types-in-generics
|
||||
* https://www.typescriptlang.org/docs/handbook/interfaces.html#difference-between-the-static-and-instance-sides-of-classes
|
||||
* for what is going on with this.
|
||||
*/
|
||||
type ParameterModelConstructor<ParameterModel extends IParameterModel> = new (
|
||||
workspace: Workspace,
|
||||
name: string,
|
||||
id: string,
|
||||
) => ParameterModel;
|
||||
interface ParameterModelConstructor<ParameterModel extends IParameterModel> {
|
||||
new (workspace: Workspace, name: string, id: string): ParameterModel;
|
||||
|
||||
/**
|
||||
* Serializes the given IProcedureModel to JSON.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function saveProcedure(proc: IProcedureModel): State {
|
||||
const state: State = {
|
||||
id: proc.getId(),
|
||||
name: proc.getName(),
|
||||
returnTypes: proc.getReturnTypes(),
|
||||
};
|
||||
if (!proc.getParameters().length) return state;
|
||||
state.parameters = proc.getParameters().map((param) => saveParameter(param));
|
||||
return state;
|
||||
/**
|
||||
* Deserializes the JSON state and returns a parameter model.
|
||||
*
|
||||
* @param state The state to deserialize.
|
||||
* @param workspace The workspace to load the parameter model into.
|
||||
* @returns The constructed parameter model.
|
||||
*/
|
||||
loadState(state: Object, workspace: Workspace): ParameterModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the given IParameterModel to JSON.
|
||||
*
|
||||
* @internal
|
||||
* Serializes the given IProcedureModel to JSON.
|
||||
*/
|
||||
export function saveParameter(param: IParameterModel): ParameterState {
|
||||
const state: ParameterState = {
|
||||
id: param.getId(),
|
||||
name: param.getName(),
|
||||
};
|
||||
if (!param.getTypes().length) return state;
|
||||
state.types = param.getTypes();
|
||||
export function saveProcedure(proc: IProcedureModel): State {
|
||||
const state: State = proc.saveState();
|
||||
if (!proc.getParameters().length) return state;
|
||||
state.parameters = proc.getParameters().map((param) => param.saveState());
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the given procedure model State from JSON.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function loadProcedure<
|
||||
ProcedureModel extends IProcedureModel,
|
||||
@@ -101,36 +89,17 @@ export function loadProcedure<
|
||||
state: State,
|
||||
workspace: Workspace,
|
||||
): ProcedureModel {
|
||||
const proc = new procedureModelClass(
|
||||
workspace,
|
||||
state.name,
|
||||
state.id,
|
||||
).setReturnTypes(state.returnTypes);
|
||||
const proc = procedureModelClass.loadState(state, workspace);
|
||||
if (!state.parameters) return proc;
|
||||
for (const [index, param] of state.parameters.entries()) {
|
||||
proc.insertParameter(
|
||||
loadParameter(parameterModelClass, param, workspace),
|
||||
parameterModelClass.loadState(param, workspace),
|
||||
index,
|
||||
);
|
||||
}
|
||||
return proc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the given ParameterState from JSON.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function loadParameter<ParameterModel extends IParameterModel>(
|
||||
parameterModelClass: ParameterModelConstructor<ParameterModel>,
|
||||
state: ParameterState,
|
||||
workspace: Workspace,
|
||||
): ParameterModel {
|
||||
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. */
|
||||
export class ProcedureSerializer<
|
||||
ProcedureModel extends IProcedureModel,
|
||||
|
||||
143
core/serialization/workspace_comments.ts
Normal file
143
core/serialization/workspace_comments.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {ISerializer} from '../interfaces/i_serializer.js';
|
||||
import {Workspace} from '../workspace.js';
|
||||
import * as priorities from './priorities.js';
|
||||
import type {WorkspaceComment} from '../comments/workspace_comment.js';
|
||||
import * as eventUtils from '../events/utils.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import * as serializationRegistry from './registry.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
|
||||
export interface State {
|
||||
id?: string;
|
||||
text?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
collapsed?: boolean;
|
||||
editable?: boolean;
|
||||
movable?: boolean;
|
||||
deletable?: boolean;
|
||||
}
|
||||
|
||||
/** Serializes the state of the given comment to JSON. */
|
||||
export function save(
|
||||
comment: WorkspaceComment,
|
||||
{
|
||||
addCoordinates = false,
|
||||
saveIds = true,
|
||||
}: {
|
||||
addCoordinates?: boolean;
|
||||
saveIds?: boolean;
|
||||
} = {},
|
||||
): State {
|
||||
const workspace = comment.workspace;
|
||||
const state: State = Object.create(null);
|
||||
|
||||
state.height = comment.getSize().height;
|
||||
state.width = comment.getSize().width;
|
||||
|
||||
if (saveIds) state.id = comment.id;
|
||||
if (addCoordinates) {
|
||||
const loc = comment.getRelativeToSurfaceXY();
|
||||
state.x = workspace.RTL ? workspace.getWidth() - loc.x : loc.x;
|
||||
state.y = loc.y;
|
||||
}
|
||||
|
||||
if (comment.getText()) state.text = comment.getText();
|
||||
if (comment.isCollapsed()) state.collapsed = true;
|
||||
if (!comment.isOwnEditable()) state.editable = false;
|
||||
if (!comment.isOwnMovable()) state.movable = false;
|
||||
if (!comment.isOwnDeletable()) state.deletable = false;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/** Appends the comment defined by the given state to the given workspace. */
|
||||
export function append(
|
||||
state: State,
|
||||
workspace: Workspace,
|
||||
{recordUndo = false}: {recordUndo?: boolean} = {},
|
||||
): WorkspaceComment {
|
||||
const prevRecordUndo = eventUtils.getRecordUndo();
|
||||
eventUtils.setRecordUndo(recordUndo);
|
||||
|
||||
const comment = workspace.newComment(state.id);
|
||||
|
||||
if (state.text !== undefined) comment.setText(state.text);
|
||||
if (state.x !== undefined || state.y !== undefined) {
|
||||
const defaultLoc = comment.getRelativeToSurfaceXY();
|
||||
let x = state.x ?? defaultLoc.x;
|
||||
x = workspace.RTL ? workspace.getWidth() - x : x;
|
||||
const y = state.y ?? defaultLoc.y;
|
||||
comment.moveTo(new Coordinate(x, y));
|
||||
}
|
||||
if (state.width !== undefined || state.height) {
|
||||
const defaultSize = comment.getSize();
|
||||
comment.setSize(
|
||||
new Size(
|
||||
state.width ?? defaultSize.width,
|
||||
state.height ?? defaultSize.height,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state.collapsed !== undefined) comment.setCollapsed(state.collapsed);
|
||||
if (state.editable !== undefined) comment.setEditable(state.editable);
|
||||
if (state.movable !== undefined) comment.setMovable(state.movable);
|
||||
if (state.deletable !== undefined) comment.setDeletable(state.deletable);
|
||||
|
||||
eventUtils.setRecordUndo(prevRecordUndo);
|
||||
|
||||
return comment;
|
||||
}
|
||||
|
||||
// Alias to disambiguate saving within the serializer.
|
||||
const saveComment = save;
|
||||
|
||||
/** Serializer for saving and loading workspace comment state. */
|
||||
export class WorkspaceCommentSerializer implements ISerializer {
|
||||
priority = priorities.WORKSPACE_COMMENTS;
|
||||
|
||||
/**
|
||||
* Returns the state of all workspace comments in the given workspace.
|
||||
*/
|
||||
save(workspace: Workspace): State[] | null {
|
||||
const commentStates = [];
|
||||
for (const comment of workspace.getTopComments()) {
|
||||
const state = saveComment(comment as AnyDuringMigration, {
|
||||
addCoordinates: true,
|
||||
saveIds: true,
|
||||
});
|
||||
if (state) commentStates.push(state);
|
||||
}
|
||||
return commentStates.length ? commentStates : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the comments defined by the given state into the given
|
||||
* workspace.
|
||||
*/
|
||||
load(state: State[], workspace: Workspace) {
|
||||
for (const commentState of state) {
|
||||
append(commentState, workspace, {recordUndo: eventUtils.getRecordUndo()});
|
||||
}
|
||||
}
|
||||
|
||||
/** Disposes of any comments that exist on the given workspace. */
|
||||
clear(workspace: Workspace) {
|
||||
for (const comment of workspace.getTopComments()) {
|
||||
comment.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serializationRegistry.register(
|
||||
'workspaceComments',
|
||||
new WorkspaceCommentSerializer(),
|
||||
);
|
||||
@@ -11,9 +11,12 @@ import * as clipboard from './clipboard.js';
|
||||
import * as common from './common.js';
|
||||
import {Gesture} from './gesture.js';
|
||||
import {ICopyData, isCopyable} from './interfaces/i_copyable.js';
|
||||
import {isDeletable} from './interfaces/i_deletable.js';
|
||||
import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js';
|
||||
import {KeyCodes} from './utils/keycodes.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import {isDraggable} from './interfaces/i_draggable.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
|
||||
/**
|
||||
* Object holding the names of the default shortcut items.
|
||||
@@ -59,6 +62,7 @@ export function registerDelete() {
|
||||
return (
|
||||
!workspace.options.readOnly &&
|
||||
selected != null &&
|
||||
isDeletable(selected) &&
|
||||
selected.isDeletable()
|
||||
);
|
||||
},
|
||||
@@ -72,7 +76,14 @@ export function registerDelete() {
|
||||
if (Gesture.inProgress()) {
|
||||
return false;
|
||||
}
|
||||
(common.getSelected() as BlockSvg).checkAndDelete();
|
||||
const selected = common.getSelected();
|
||||
if (selected instanceof BlockSvg) {
|
||||
selected.checkAndDelete();
|
||||
} else if (isDeletable(selected) && selected.isDeletable()) {
|
||||
eventUtils.setGroup(true);
|
||||
selected.dispose();
|
||||
eventUtils.setGroup(false);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
keyCodes: [KeyCodes.DELETE, KeyCodes.BACKSPACE],
|
||||
@@ -105,7 +116,9 @@ export function registerCopy() {
|
||||
!workspace.options.readOnly &&
|
||||
!Gesture.inProgress() &&
|
||||
selected != null &&
|
||||
isDeletable(selected) &&
|
||||
selected.isDeletable() &&
|
||||
isDraggable(selected) &&
|
||||
selected.isMovable() &&
|
||||
isCopyable(selected)
|
||||
);
|
||||
@@ -148,19 +161,32 @@ export function registerCut() {
|
||||
!workspace.options.readOnly &&
|
||||
!Gesture.inProgress() &&
|
||||
selected != null &&
|
||||
selected instanceof BlockSvg &&
|
||||
isDeletable(selected) &&
|
||||
selected.isDeletable() &&
|
||||
isDraggable(selected) &&
|
||||
selected.isMovable() &&
|
||||
!selected.workspace!.isFlyout
|
||||
);
|
||||
},
|
||||
callback(workspace) {
|
||||
const selected = common.getSelected();
|
||||
if (!selected || !isCopyable(selected)) return false;
|
||||
copyData = selected.toCopyData();
|
||||
copyWorkspace = workspace;
|
||||
(selected as BlockSvg).checkAndDelete();
|
||||
return true;
|
||||
|
||||
if (selected instanceof BlockSvg) {
|
||||
copyData = selected.toCopyData();
|
||||
copyWorkspace = workspace;
|
||||
selected.checkAndDelete();
|
||||
return true;
|
||||
} else if (
|
||||
isDeletable(selected) &&
|
||||
selected.isDeletable() &&
|
||||
isCopyable(selected)
|
||||
) {
|
||||
copyData = selected.toCopyData();
|
||||
copyWorkspace = workspace;
|
||||
selected.dispose();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
keyCodes: [ctrlX, altX, metaX],
|
||||
};
|
||||
@@ -216,10 +242,11 @@ export function registerUndo() {
|
||||
preconditionFn(workspace) {
|
||||
return !workspace.options.readOnly && !Gesture.inProgress();
|
||||
},
|
||||
callback(workspace) {
|
||||
callback(workspace, e) {
|
||||
// 'z' for undo 'Z' is for redo.
|
||||
(workspace as WorkspaceSvg).hideChaff();
|
||||
workspace.undo(false);
|
||||
e.preventDefault();
|
||||
return true;
|
||||
},
|
||||
keyCodes: [ctrlZ, altZ, metaZ],
|
||||
@@ -254,10 +281,11 @@ export function registerRedo() {
|
||||
preconditionFn(workspace) {
|
||||
return !Gesture.inProgress() && !workspace.options.readOnly;
|
||||
},
|
||||
callback(workspace) {
|
||||
callback(workspace, e) {
|
||||
// 'z' for undo 'Z' is for redo.
|
||||
(workspace as WorkspaceSvg).hideChaff();
|
||||
workspace.undo(true);
|
||||
e.preventDefault();
|
||||
return true;
|
||||
},
|
||||
keyCodes: [ctrlShiftZ, altShiftZ, metaShiftZ, ctrlY],
|
||||
|
||||
@@ -41,9 +41,9 @@ import * as dom from '../utils/dom.js';
|
||||
import {Rect} from '../utils/rect.js';
|
||||
import * as toolbox from '../utils/toolbox.js';
|
||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
||||
|
||||
import type {ToolboxCategory} from './category.js';
|
||||
import {CollapsibleToolboxCategory} from './collapsible_category.js';
|
||||
import {isDeletable} from '../interfaces/i_deletable.js';
|
||||
|
||||
/**
|
||||
* Class for a Toolbox.
|
||||
@@ -532,17 +532,15 @@ export class Toolbox
|
||||
* before onDragEnter/onDragOver/onDragExit.
|
||||
*
|
||||
* @param element The block or bubble currently being dragged.
|
||||
* @param _couldConnect Whether the element could could connect to another.
|
||||
* @returns Whether the element provided would be deleted if dropped on this
|
||||
* area.
|
||||
*/
|
||||
override wouldDelete(element: IDraggable, _couldConnect: boolean): boolean {
|
||||
override wouldDelete(element: IDraggable): boolean {
|
||||
if (element instanceof BlockSvg) {
|
||||
const block = element;
|
||||
// Prefer dragging to the toolbox over connecting to other blocks.
|
||||
this.updateWouldDelete_(!block.getParent() && block.isDeletable());
|
||||
} else {
|
||||
this.updateWouldDelete_(element.isDeletable());
|
||||
this.updateWouldDelete_(isDeletable(element) && element.isDeletable());
|
||||
}
|
||||
return this.wouldDelete_;
|
||||
}
|
||||
|
||||
@@ -656,6 +656,7 @@ export class Trashcan
|
||||
delete json['x'];
|
||||
delete json['y'];
|
||||
delete json['enabled'];
|
||||
delete json['disabledReasons'];
|
||||
|
||||
if (json['icons'] && json['icons']['comment']) {
|
||||
const comment = json['icons']['comment'];
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
// Former goog.module ID: Blockly.utils.dom
|
||||
|
||||
import * as deprecation from './deprecation.js';
|
||||
import type {Svg} from './svg.js';
|
||||
|
||||
/**
|
||||
@@ -154,24 +153,6 @@ export function insertAfter(newNode: Element, refNode: Element) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a node contains another node.
|
||||
*
|
||||
* @param parent The node that should contain the other node.
|
||||
* @param descendant The node to test presence of.
|
||||
* @returns Whether the parent node contains the descendant node.
|
||||
* @deprecated Use native 'contains' DOM method.
|
||||
*/
|
||||
export function containsNode(parent: Node, descendant: Node): boolean {
|
||||
deprecation.warn(
|
||||
'Blockly.utils.dom.containsNode',
|
||||
'version 10',
|
||||
'version 11',
|
||||
'Use native "contains" DOM method',
|
||||
);
|
||||
return parent.contains(descendant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the CSS transform property on an element. This function sets the
|
||||
* non-vendor-prefixed and vendor-prefixed versions for backwards compatibility
|
||||
|
||||
@@ -43,4 +43,20 @@ export class Size {
|
||||
}
|
||||
return a.width === b.width && a.height === b.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new size with the maximum width and height values out of both
|
||||
* sizes.
|
||||
*/
|
||||
static max(a: Size, b: Size): Size {
|
||||
return new Size(Math.max(a.width, b.width), Math.max(a.height, b.height));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new size with the minimum width and height values out of both
|
||||
* sizes.
|
||||
*/
|
||||
static min(a: Size, b: Size): Size {
|
||||
return new Size(Math.min(a.width, b.width), Math.min(a.height, b.height));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,27 +6,6 @@
|
||||
|
||||
// Former goog.module ID: Blockly.utils.string
|
||||
|
||||
import * as deprecation from './deprecation.js';
|
||||
|
||||
/**
|
||||
* Fast prefix-checker.
|
||||
* Copied from Closure's goog.string.startsWith.
|
||||
*
|
||||
* @param str The string to check.
|
||||
* @param prefix A string to look for at the start of `str`.
|
||||
* @returns True if `str` begins with `prefix`.
|
||||
* @deprecated Use built-in **string.startsWith** instead.
|
||||
*/
|
||||
export function startsWith(str: string, prefix: string): boolean {
|
||||
deprecation.warn(
|
||||
'Blockly.utils.string.startsWith()',
|
||||
'April 2022',
|
||||
'April 2023',
|
||||
'Use built-in string.startsWith',
|
||||
);
|
||||
return str.startsWith(prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of strings, return the length of the shortest one.
|
||||
*
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface BlockInfo {
|
||||
type?: string;
|
||||
gap?: string | number;
|
||||
disabled?: string | boolean;
|
||||
disabledReasons?: string[];
|
||||
enabled?: boolean;
|
||||
id?: string;
|
||||
x?: number;
|
||||
|
||||
@@ -30,7 +30,7 @@ import * as math from './utils/math.js';
|
||||
import type * as toolbox from './utils/toolbox.js';
|
||||
import {VariableMap} from './variable_map.js';
|
||||
import type {VariableModel} from './variable_model.js';
|
||||
import type {WorkspaceComment} from './workspace_comment.js';
|
||||
import {WorkspaceComment} from './comments/workspace_comment.js';
|
||||
import {IProcedureMap} from './interfaces/i_procedure_map.js';
|
||||
import {ObservableProcedureMap} from './observable_procedure_map.js';
|
||||
|
||||
@@ -515,6 +515,20 @@ export class Workspace implements IASTNodeLocation {
|
||||
'monkey-patched in by blockly.ts',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a newly created comment.
|
||||
*
|
||||
* @param id Optional ID. Use this ID if provided, otherwise create a new
|
||||
* ID.
|
||||
* @returns The created comment.
|
||||
*/
|
||||
newComment(id?: string): WorkspaceComment {
|
||||
throw new Error(
|
||||
'The implementation of newComment should be ' +
|
||||
'monkey-patched in by blockly.ts',
|
||||
);
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user