diff --git a/blocks/blocks.ts b/blocks/blocks.ts index f4a9936ed..a9874e54d 100644 --- a/blocks/blocks.ts +++ b/blocks/blocks.ts @@ -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, diff --git a/blocks/colour.ts b/blocks/colour.ts deleted file mode 100644 index e57e4ba9b..000000000 --- a/blocks/colour.ts +++ /dev/null @@ -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); diff --git a/blocks/loops.ts b/blocks/loops.ts index 02d9d34be..c7cb710d7 100644 --- a/blocks/loops.ts +++ b/blocks/loops.ts @@ -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); + } } }, }; diff --git a/blocks/procedures.ts b/blocks/procedures.ts index f7e3bd62d..1214eb55e 100644 --- a/blocks/procedures.ts +++ b/blocks/procedures.ts @@ -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); + } } }, /** diff --git a/blocks/text.ts b/blocks/text.ts index 0381e5493..91a27005a 100644 --- a/blocks/text.ts +++ b/blocks/text.ts @@ -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': '', diff --git a/core/block.ts b/core/block.ts index 83485fc7b..52191d63c 100644 --- a/core/block.ts +++ b/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(); 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 { + 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( diff --git a/core/block_dragger.ts b/core/block_dragger.ts deleted file mode 100644 index 972c5fa2b..000000000 --- a/core/block_dragger.ts +++ /dev/null @@ -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); diff --git a/core/block_svg.ts b/core/block_svg.ts index f8f0c02ff..e328f67ee 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -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, - 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(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): 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]; + } } diff --git a/core/blockly.ts b/core/blockly.ts index d9fb8f993..cee509480 100644 --- a/core/blockly.ts +++ b/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}; diff --git a/core/bubble_dragger.ts b/core/bubble_dragger.ts deleted file mode 100644 index e494c7ad2..000000000 --- a/core/bubble_dragger.ts +++ /dev/null @@ -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; - } -} diff --git a/core/bubbles/bubble.ts b/core/bubbles/bubble.ts index 4bea9d863..35b9e7dde 100644 --- a/core/bubbles/bubble.ts +++ b/core/bubbles/bubble.ts @@ -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. + } } diff --git a/core/bubbles/mini_workspace_bubble.ts b/core/bubbles/mini_workspace_bubble.ts index f459654fa..74317d57b 100644 --- a/core/bubbles/mini_workspace_bubble.ts +++ b/core/bubbles/mini_workspace_bubble.ts @@ -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, ) { diff --git a/core/bubbles/text_bubble.ts b/core/bubbles/text_bubble.ts index 6f50d303b..020ab4f2e 100644 --- a/core/bubbles/text_bubble.ts +++ b/core/bubbles/text_bubble.ts @@ -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, ) { diff --git a/core/bubbles/textinput_bubble.ts b/core/bubbles/textinput_bubble.ts index 081f86097..2784b5cb6 100644 --- a/core/bubbles/textinput_bubble.ts +++ b/core/bubbles/textinput_bubble.ts @@ -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; diff --git a/core/bump_objects.ts b/core/bump_objects.ts index d06b66800..3ceae2dbc 100644 --- a/core/bump_objects.ts +++ b/core/bump_objects.ts @@ -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; diff --git a/core/clipboard.ts b/core/clipboard.ts index 2c3872c1a..ed574d112 100644 --- a/core/clipboard.ts +++ b/core/clipboard.ts @@ -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(toCopy: ICopyable): 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( ?.paste(copyData, workspace, coordinate) ?? null) as ICopyable | 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 & 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. */ diff --git a/core/clipboard/block_paster.ts b/core/clipboard/block_paster.ts index 0bb707c36..fefc9947d 100644 --- a/core/clipboard/block_paster.ts +++ b/core/clipboard/block_paster.ts @@ -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 { static TYPE = 'block'; @@ -43,7 +44,7 @@ export class BlockPaster implements IPaster { if (eventUtils.isEnabled() && !block.isShadow()) { eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block)); } - block.select(); + common.setSelected(block); return block; } } diff --git a/core/clipboard/workspace_comment_paster.ts b/core/clipboard/workspace_comment_paster.ts index aeedbfb2b..c7e5eed68 100644 --- a/core/clipboard/workspace_comment_paster.ts +++ b/core/clipboard/workspace_comment_paster.ts @@ -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 + implements IPaster { 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()); diff --git a/core/comments.ts b/core/comments.ts new file mode 100644 index 000000000..368db0e77 --- /dev/null +++ b/core/comments.ts @@ -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'; diff --git a/core/comments/comment_view.ts b/core/comments/comment_view.ts new file mode 100644 index 000000000..553f7d02b --- /dev/null +++ b/core/comments/comment_view.ts @@ -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; +} +`); diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts new file mode 100644 index 000000000..a2b4d689f --- /dev/null +++ b/core/comments/rendered_workspace_comment.ts @@ -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, + 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']); + } + } +} diff --git a/core/comments/workspace_comment.ts b/core/comments/workspace_comment.ts new file mode 100644 index 000000000..3c23aba86 --- /dev/null +++ b/core/comments/workspace_comment.ts @@ -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; + } +} diff --git a/core/common.ts b/core/common.ts index 625921350..fba960a5b 100644 --- a/core/common.ts +++ b/core/common.ts @@ -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(); } /** diff --git a/core/config.ts b/core/config.ts index a6642c266..9def1dca4 100644 --- a/core/config.ts +++ b/core/config.ts @@ -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, /** diff --git a/core/connection.ts b/core/connection.ts index 7a8836822..1dd8dc1ea 100644 --- a/core/connection.ts +++ b/core/connection.ts @@ -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()); diff --git a/core/constants.ts b/core/constants.ts index 7c3312f29..538bd3783 100644 --- a/core/constants.ts +++ b/core/constants.ts @@ -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'; diff --git a/core/contextmenu.ts b/core/contextmenu.ts index 78431c93f..939477b3c 100644 --- a/core/contextmenu.ts +++ b/core/contextmenu.ts @@ -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; -} diff --git a/core/contextmenu_items.ts b/core/contextmenu_items.ts index a540a1341..254906ce7 100644 --- a/core/contextmenu_items.ts +++ b/core/contextmenu_items.ts @@ -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. diff --git a/core/contextmenu_registry.ts b/core/contextmenu_registry.ts index c1183ca97..abbd0f975 100644 --- a/core/contextmenu_registry.ts +++ b/core/contextmenu_registry.ts @@ -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; } diff --git a/core/css.ts b/core/css.ts index 07e9c98a4..9940c9fad 100644 --- a/core/css.ts +++ b/core/css.ts @@ -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 diff --git a/core/delete_area.ts b/core/delete_area.ts index ca47c1a9e..4967927c4 100644 --- a/core/delete_area.ts +++ b/core/delete_area.ts @@ -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_; } diff --git a/core/drag_target.ts b/core/drag_target.ts index 3f58bdc56..e973f2dd1 100644 --- a/core/drag_target.ts +++ b/core/drag_target.ts @@ -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 diff --git a/core/dragging.ts b/core/dragging.ts new file mode 100644 index 000000000..a7e46fc27 --- /dev/null +++ b/core/dragging.ts @@ -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}; diff --git a/core/dragging/block_drag_strategy.ts b/core/dragging/block_drag_strategy.ts new file mode 100644 index 000000000..fadba28fb --- /dev/null +++ b/core/dragging/block_drag_strategy.ts @@ -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; + } +} diff --git a/core/dragging/bubble_drag_strategy.ts b/core/dragging/bubble_drag_strategy.ts new file mode 100644 index 000000000..7ffccddc1 --- /dev/null +++ b/core/dragging/bubble_drag_strategy.ts @@ -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); + } +} diff --git a/core/dragging/comment_drag_strategy.ts b/core/dragging/comment_drag_strategy.ts new file mode 100644 index 000000000..5fe5e356c --- /dev/null +++ b/core/dragging/comment_drag_strategy.ts @@ -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); + } +} diff --git a/core/dragging/dragger.ts b/core/dragging/dragger.ts new file mode 100644 index 000000000..25266799e --- /dev/null +++ b/core/dragging/dragger.ts @@ -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); diff --git a/core/events/events.ts b/core/events/events.ts index 6f16387fd..bb8011755 100644 --- a/core/events/events.ts +++ b/core/events/events.ts @@ -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}; diff --git a/core/events/events_block_change.ts b/core/events/events_block_change.ts index 3ef5c1085..570b7f58e 100644 --- a/core/events/events_block_change.ts +++ b/core/events/events_block_change.ts @@ -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); diff --git a/core/events/events_comment_base.ts b/core/events/events_comment_base.ts index fe60d0d92..6fbc95c4d 100644 --- a/core/events/events_comment_base.ts +++ b/core/events/events_comment_base.ts @@ -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); } } } diff --git a/core/events/events_comment_change.ts b/core/events/events_comment_change.ts index be0e285a5..eb39d929d 100644 --- a/core/events/events_comment_change.ts +++ b/core/events/events_comment_change.ts @@ -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); } } diff --git a/core/events/events_comment_collapse.ts b/core/events/events_comment_collapse.ts new file mode 100644 index 000000000..6646b1df2 --- /dev/null +++ b/core/events/events_comment_collapse.ts @@ -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, +); diff --git a/core/events/events_comment_create.ts b/core/events/events_comment_create.ts index 4db859c7b..692397df6 100644 --- a/core/events/events_comment_create.ts +++ b/core/events/events_comment_create.ts @@ -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( diff --git a/core/events/events_comment_delete.ts b/core/events/events_comment_delete.ts index ee08c602c..62f8916fb 100644 --- a/core/events/events_comment_delete.ts +++ b/core/events/events_comment_delete.ts @@ -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( diff --git a/core/events/events_comment_move.ts b/core/events/events_comment_move.ts index 013064a5d..502ca032f 100644 --- a/core/events/events_comment_move.ts +++ b/core/events/events_comment_move.ts @@ -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); } } diff --git a/core/events/utils.ts b/core/events/utils.ts index 469f10582..eacf04906 100644 --- a/core/events/utils.ts +++ b/core/events/utils.ts @@ -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); } diff --git a/core/field.ts b/core/field.ts index 58f120cb8..51e006823 100644 --- a/core/field.ts +++ b/core/field.ts @@ -314,6 +314,7 @@ export abstract class Field this.setTooltip(this.tooltip_); this.bindEvents_(); this.initModel(); + this.applyColour(); } /** @@ -1062,7 +1063,6 @@ export abstract class Field this.isDirty_ = true; if (this.sourceBlock_ && this.sourceBlock_.rendered) { (this.sourceBlock_ as BlockSvg).queueRender(); - (this.sourceBlock_ as BlockSvg).bumpNeighbours(); } } diff --git a/core/field_angle.ts b/core/field_angle.ts deleted file mode 100644 index 7eef3099f..000000000 --- a/core/field_angle.ts +++ /dev/null @@ -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 { - /** 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; diff --git a/core/field_colour.ts b/core/field_colour.ts deleted file mode 100644 index 46bf3f0e2..000000000 --- a/core/field_colour.ts +++ /dev/null @@ -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 { - /** - * 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; diff --git a/core/field_image.ts b/core/field_image.ts index 09461a790..6e83e3405 100644 --- a/core/field_image.ts +++ b/core/field_image.ts @@ -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; diff --git a/core/field_multilineinput.ts b/core/field_multilineinput.ts deleted file mode 100644 index b1ce0cb10..000000000 --- a/core/field_multilineinput.ts +++ /dev/null @@ -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; diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 6d8760b7f..f9e6545a9 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -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. * diff --git a/core/generator.ts b/core/generator.ts index f948924d9..7bb509f8b 100644 --- a/core/generator.ts +++ b/core/generator.ts @@ -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 ` + diff --git a/core/gesture.ts b/core/gesture.ts index 4b85c4f6f..7970ed006 100644 --- a/core/gesture.ts +++ b/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? * diff --git a/core/grid.ts b/core/grid.ts index e495e220f..1a5de250e 100644 --- a/core/grid.ts +++ b/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. * diff --git a/core/icons/comment_icon.ts b/core/icons/comment_icon.ts index 7756088b6..df54560c5 100644 --- a/core/icons/comment_icon.ts +++ b/core/icons/comment_icon.ts @@ -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 { 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 { + 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; } diff --git a/core/icons/icon.ts b/core/icons/icon.ts index b1104b157..6ad953236 100644 --- a/core/icons/icon.ts +++ b/core/icons/icon.ts @@ -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); - } } diff --git a/core/icons/icon_types.ts b/core/icons/icon_types.ts index 25773c5bb..c5edb0f74 100644 --- a/core/icons/icon_types.ts +++ b/core/icons/icon_types.ts @@ -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('mutator'); static WARNING = new IconType('warning'); - static COMMENT = new IconType('comment'); + static COMMENT = new IconType('comment'); } diff --git a/core/icons/mutator_icon.ts b/core/icons/mutator_icon.ts index 56365ed1f..7fb3fcf3b 100644 --- a/core/icons/mutator_icon.ts +++ b/core/icons/mutator_icon.ts @@ -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 { 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(); - } } diff --git a/core/icons/warning_icon.ts b/core/icons/warning_icon.ts index f0862c3e9..08f511a60 100644 --- a/core/icons/warning_icon.ts +++ b/core/icons/warning_icon.ts @@ -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 { if (this.bubbleIsVisible() === visible) return; + await renderManagement.finishQueuedRenders(); + if (visible) { this.textBubble = new TextBubble( this.getText(), diff --git a/core/inject.ts b/core/inject.ts index b938abaa4..323191817 100644 --- a/core/inject.ts +++ b/core/inject.ts @@ -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']); diff --git a/core/inputs/input.ts b/core/inputs/input.ts index 767c07965..da7cccad5 100644 --- a/core/inputs/input.ts +++ b/core/inputs/input.ts @@ -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; diff --git a/core/insertion_marker_manager.ts b/core/insertion_marker_manager.ts index 96c9327fc..376297f10 100644 --- a/core/insertion_marker_manager.ts +++ b/core/insertion_marker_manager.ts @@ -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; diff --git a/core/insertion_marker_previewer.ts b/core/insertion_marker_previewer.ts index 1004b7b08..6ed278c40 100644 --- a/core/insertion_marker_previewer.ts +++ b/core/insertion_marker_previewer.ts @@ -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; diff --git a/core/interfaces/i_block_dragger.ts b/core/interfaces/i_block_dragger.ts deleted file mode 100644 index 5c6a9d112..000000000 --- a/core/interfaces/i_block_dragger.ts +++ /dev/null @@ -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; -} diff --git a/core/interfaces/i_comment_icon.ts b/core/interfaces/i_comment_icon.ts new file mode 100644 index 000000000..09b071110 --- /dev/null +++ b/core/interfaces/i_comment_icon.ts @@ -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 + ); +} diff --git a/core/interfaces/i_deletable.ts b/core/interfaces/i_deletable.ts index 89cc08616..046770940 100644 --- a/core/interfaces/i_deletable.ts +++ b/core/interfaces/i_deletable.ts @@ -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 + ); } diff --git a/core/interfaces/i_delete_area.ts b/core/interfaces/i_delete_area.ts index 82d3907c5..86d2673bb 100644 --- a/core/interfaces/i_delete_area.ts +++ b/core/interfaces/i_delete_area.ts @@ -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; } diff --git a/core/interfaces/i_draggable.ts b/core/interfaces/i_draggable.ts index 21ada1055..cb723e7b8 100644 --- a/core/interfaces/i_draggable.ts +++ b/core/interfaces/i_draggable.ts @@ -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 + ); +} diff --git a/core/interfaces/i_dragger.ts b/core/interfaces/i_dragger.ts new file mode 100644 index 000000000..1e8ad0ab6 --- /dev/null +++ b/core/interfaces/i_dragger.ts @@ -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; +} diff --git a/core/interfaces/i_has_bubble.ts b/core/interfaces/i_has_bubble.ts index a2ba6093a..276feff21 100644 --- a/core/interfaces/i_has_bubble.ts +++ b/core/interfaces/i_has_bubble.ts @@ -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; } /** Type guard that checks whether the given object is a IHasBubble. */ diff --git a/core/interfaces/i_parameter_model.ts b/core/interfaces/i_parameter_model.ts index 1b0239703..6b351b6b3 100644 --- a/core/interfaces/i_parameter_model.ts +++ b/core/interfaces/i_parameter_model.ts @@ -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; } diff --git a/core/interfaces/i_procedure_model.ts b/core/interfaces/i_procedure_model.ts index cb5fda09f..61026adae 100644 --- a/core/interfaces/i_procedure_model.ts +++ b/core/interfaces/i_procedure_model.ts @@ -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; } diff --git a/core/interfaces/i_selectable.ts b/core/interfaces/i_selectable.ts index cda47c7cd..7cf9ad98c 100644 --- a/core/interfaces/i_selectable.ts +++ b/core/interfaces/i_selectable.ts @@ -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 + ); +} diff --git a/core/internal_constants.ts b/core/internal_constants.ts index 2dd72c82f..27c945dc0 100644 --- a/core/internal_constants.ts +++ b/core/internal_constants.ts @@ -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. * diff --git a/core/main.ts b/core/main.ts index f794a8ab3..c301e9890 100644 --- a/core/main.ts +++ b/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 diff --git a/core/options.ts b/core/options.ts index 3b6483a26..42d2b41de 100644 --- a/core/options.ts +++ b/core/options.ts @@ -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; diff --git a/core/registry.ts b/core/registry.ts index 727711d48..d46c36f48 100644 --- a/core/registry.ts +++ b/core/registry.ts @@ -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('metricsManager'); - static BLOCK_DRAGGER = new Type('blockDragger'); + /** + * Type for an IDragger. Formerly behavior was mostly covered by + * BlockDraggeers, which is why the name is inaccurate. + */ + static BLOCK_DRAGGER = new Type('blockDragger'); /** @internal */ static SERIALIZER = new Type('serializer'); diff --git a/core/render_management.ts b/core/render_management.ts index c9041aedc..3e84efead 100644 --- a/core/render_management.ts +++ b/core/render_management.ts @@ -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(); /** The set of all blocks in need of rendering. */ const dirtyBlocks = new WeakSet(); +/** A map from queued blocks to the event group from when they were queued. */ +const eventGroups = new WeakMap(); + /** * 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); } diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index 8953ec977..5aa6f7f75 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -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; } } diff --git a/core/renderers/common/drawer.ts b/core/renderers/common/drawer.ts index 83732516c..5ded620b7 100644 --- a/core/renderers/common/drawer.ts +++ b/core/renderers/common/drawer.ts @@ -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 diff --git a/core/renderers/measurables/icon.ts b/core/renderers/measurables/icon.ts index d681dbbd9..98e3f722d 100644 --- a/core/renderers/measurables/icon.ts +++ b/core/renderers/measurables/icon.ts @@ -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(); diff --git a/core/renderers/minimalist/constants.ts b/core/renderers/minimalist/constants.ts deleted file mode 100644 index 83bfaff0a..000000000 --- a/core/renderers/minimalist/constants.ts +++ /dev/null @@ -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', - ); - } -} diff --git a/core/renderers/minimalist/drawer.ts b/core/renderers/minimalist/drawer.ts deleted file mode 100644 index d0985674c..000000000 --- a/core/renderers/minimalist/drawer.ts +++ /dev/null @@ -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', - ); - } -} diff --git a/core/renderers/minimalist/info.ts b/core/renderers/minimalist/info.ts deleted file mode 100644 index ae86e3eb6..000000000 --- a/core/renderers/minimalist/info.ts +++ /dev/null @@ -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_; - } -} diff --git a/core/renderers/minimalist/minimalist.ts b/core/renderers/minimalist/minimalist.ts deleted file mode 100644 index 4ec0a220c..000000000 --- a/core/renderers/minimalist/minimalist.ts +++ /dev/null @@ -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}; diff --git a/core/renderers/minimalist/renderer.ts b/core/renderers/minimalist/renderer.ts deleted file mode 100644 index b15b48f19..000000000 --- a/core/renderers/minimalist/renderer.ts +++ /dev/null @@ -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); diff --git a/core/serialization.ts b/core/serialization.ts index 8362f2ced..8e159bb2b 100644 --- a/core/serialization.ts +++ b/core/serialization.ts @@ -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, }; diff --git a/core/serialization/blocks.ts b/core/serialization/blocks.ts index 229686a0d..be5960965 100644 --- a/core/serialization/blocks.ts +++ b/core/serialization/blocks.ts @@ -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']); diff --git a/core/serialization/priorities.ts b/core/serialization/priorities.ts index c0e781166..726242f01 100644 --- a/core/serialization/priorities.ts +++ b/core/serialization/priorities.ts @@ -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; diff --git a/core/serialization/procedures.ts b/core/serialization/procedures.ts index 34d381171..55d720604 100644 --- a/core/serialization/procedures.ts +++ b/core/serialization/procedures.ts @@ -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 = new ( - workspace: Workspace, - name: string, - id: string, -) => ProcedureModel; +interface ProcedureModelConstructor { + 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 = new ( - workspace: Workspace, - name: string, - id: string, -) => ParameterModel; +interface ParameterModelConstructor { + 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( - parameterModelClass: ParameterModelConstructor, - 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, diff --git a/core/serialization/workspace_comments.ts b/core/serialization/workspace_comments.ts new file mode 100644 index 000000000..525274e58 --- /dev/null +++ b/core/serialization/workspace_comments.ts @@ -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(), +); diff --git a/core/shortcut_items.ts b/core/shortcut_items.ts index 57b90a498..b5abf5554 100644 --- a/core/shortcut_items.ts +++ b/core/shortcut_items.ts @@ -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], diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 19a88184c..e0fb62e23 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -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_; } diff --git a/core/trashcan.ts b/core/trashcan.ts index b48777c4d..050f506a4 100644 --- a/core/trashcan.ts +++ b/core/trashcan.ts @@ -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']; diff --git a/core/utils/dom.ts b/core/utils/dom.ts index e9efca9f8..e318e7e91 100644 --- a/core/utils/dom.ts +++ b/core/utils/dom.ts @@ -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 diff --git a/core/utils/size.ts b/core/utils/size.ts index ab88d2c84..705dc2c28 100644 --- a/core/utils/size.ts +++ b/core/utils/size.ts @@ -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)); + } } diff --git a/core/utils/string.ts b/core/utils/string.ts index 25a3a8bbc..b7842c948 100644 --- a/core/utils/string.ts +++ b/core/utils/string.ts @@ -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. * diff --git a/core/utils/toolbox.ts b/core/utils/toolbox.ts index 17fd327c6..fbf031ac6 100644 --- a/core/utils/toolbox.ts +++ b/core/utils/toolbox.ts @@ -21,6 +21,7 @@ export interface BlockInfo { type?: string; gap?: string | number; disabled?: string | boolean; + disabledReasons?: string[]; enabled?: boolean; id?: string; x?: number; diff --git a/core/workspace.ts b/core/workspace.ts index dbbcb7743..2e9b2b0a1 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -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 */ /** diff --git a/core/workspace_comment.ts b/core/workspace_comment.ts deleted file mode 100644 index 4149bcff4..000000000 --- a/core/workspace_comment.ts +++ /dev/null @@ -1,397 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Object representing a code comment on the workspace. - * - * @class - */ -// Former goog.module ID: Blockly.WorkspaceComment - -import type {CommentMove} from './events/events_comment_move.js'; -import * as eventUtils from './events/utils.js'; -import {Coordinate} from './utils/coordinate.js'; -import * as idGenerator from './utils/idgenerator.js'; -import * as xml from './utils/xml.js'; -import type {Workspace} from './workspace.js'; - -/** - * Class for a workspace comment. - */ -export class WorkspaceComment { - id: string; - protected xy_: Coordinate; - protected height_: number; - protected width_: number; - protected RTL: boolean; - - private deletable = true; - - private movable = true; - - private editable = true; - protected content_: string; - - /** Whether this comment has been disposed. */ - protected disposed_ = false; - /** @internal */ - isComment = true; - - /** - * @param workspace The block's workspace. - * @param content The content of this workspace comment. - * @param height Height of the comment. - * @param width Width of the comment. - * @param opt_id Optional ID. Use this ID if provided, otherwise create a new - * ID. - */ - constructor( - public workspace: Workspace, - content: string, - height: number, - width: number, - opt_id?: string, - ) { - this.id = - opt_id && !workspace.getCommentById(opt_id) - ? opt_id - : idGenerator.genUid(); - - workspace.addTopComment(this); - - /** - * The comment's position in workspace units. (0, 0) is at the workspace's - * origin; scale does not change this value. - */ - this.xy_ = new Coordinate(0, 0); - - /** - * The comment's height in workspace units. Scale does not change this - * value. - */ - this.height_ = height; - - /** - * The comment's width in workspace units. Scale does not change this - * value. - */ - this.width_ = width; - - this.RTL = workspace.RTL; - - this.content_ = content; - - WorkspaceComment.fireCreateEvent(this); - } - - /** - * Dispose of this comment. - * - * @internal - */ - dispose() { - if (this.disposed_) { - return; - } - - if (eventUtils.isEnabled()) { - eventUtils.fire(new (eventUtils.get(eventUtils.COMMENT_DELETE))(this)); - } - // Remove from the list of top comments and the comment database. - this.workspace.removeTopComment(this); - this.disposed_ = true; - } - - // Height, width, x, and y are all stored on even non-rendered comments, to - // preserve state if you pass the contents through a headless workspace. - - /** - * Get comment height. - * - * @returns Comment height. - * @internal - */ - getHeight(): number { - return this.height_; - } - - /** - * Set comment height. - * - * @param height Comment height. - * @internal - */ - setHeight(height: number) { - this.height_ = height; - } - - /** - * Get comment width. - * - * @returns Comment width. - * @internal - */ - getWidth(): number { - return this.width_; - } - - /** - * Set comment width. - * - * @param width comment width. - * @internal - */ - setWidth(width: number) { - this.width_ = width; - } - - /** - * Get stored location. - * - * @returns The comment's stored location. - * This is not valid if the comment is currently being dragged. - * @internal - */ - getRelativeToSurfaceXY(): Coordinate { - return new Coordinate(this.xy_.x, this.xy_.y); - } - - /** - * Move a comment by a relative offset. - * - * @param dx Horizontal offset, in workspace units. - * @param dy Vertical offset, in workspace units. - * @internal - */ - moveBy(dx: number, dy: number) { - const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))( - this, - ) as CommentMove; - this.xy_.translate(dx, dy); - event.recordNew(); - eventUtils.fire(event); - } - - /** - * Get whether this comment is deletable or not. - * - * @returns True if deletable. - * @internal - */ - isDeletable(): boolean { - return ( - this.deletable && !(this.workspace && this.workspace.options.readOnly) - ); - } - - /** - * Set whether this comment is deletable or not. - * - * @param deletable True if deletable. - * @internal - */ - setDeletable(deletable: boolean) { - this.deletable = deletable; - } - - /** - * Get whether this comment is movable or not. - * - * @returns True if movable. - * @internal - */ - isMovable(): boolean { - return this.movable && !(this.workspace && this.workspace.options.readOnly); - } - - /** - * Set whether this comment is movable or not. - * - * @param movable True if movable. - * @internal - */ - setMovable(movable: boolean) { - this.movable = movable; - } - - /** - * Get whether this comment is editable or not. - * - * @returns True if editable. - */ - isEditable(): boolean { - return ( - this.editable && !(this.workspace && this.workspace.options.readOnly) - ); - } - - /** - * Set whether this comment is editable or not. - * - * @param editable True if editable. - */ - setEditable(editable: boolean) { - this.editable = editable; - } - - /** - * Returns this comment's text. - * - * @returns Comment text. - * @internal - */ - getContent(): string { - return this.content_; - } - - /** - * Set this comment's content. - * - * @param content Comment content. - * @internal - */ - setContent(content: string) { - if (this.content_ !== content) { - eventUtils.fire( - new (eventUtils.get(eventUtils.COMMENT_CHANGE))( - this, - this.content_, - content, - ), - ); - this.content_ = content; - } - } - - /** - * Encode a comment subtree as XML with XY coordinates. - * - * @param opt_noId True if the encoder should skip the comment ID. - * @returns Tree of XML elements. - * @internal - */ - toXmlWithXY(opt_noId?: boolean): Element { - const element = this.toXml(opt_noId); - element.setAttribute('x', String(Math.round(this.xy_.x))); - element.setAttribute('y', String(Math.round(this.xy_.y))); - element.setAttribute('h', String(this.height_)); - element.setAttribute('w', String(this.width_)); - return element; - } - - /** - * Encode a comment subtree as XML, but don't serialize the XY coordinates. - * This method avoids some expensive metrics-related calls that are made in - * toXmlWithXY(). - * - * @param opt_noId True if the encoder should skip the comment ID. - * @returns Tree of XML elements. - * @internal - */ - toXml(opt_noId?: boolean): Element { - const commentElement = xml.createElement('comment'); - if (!opt_noId) { - commentElement.id = this.id; - } - commentElement.textContent = this.getContent(); - return commentElement; - } - - /** - * Fire a create event for the given workspace comment, if comments are - * enabled. - * - * @param comment The comment that was just created. - * @internal - */ - static fireCreateEvent(comment: WorkspaceComment) { - if (eventUtils.isEnabled()) { - const existingGroup = eventUtils.getGroup(); - if (!existingGroup) { - eventUtils.setGroup(true); - } - try { - eventUtils.fire( - new (eventUtils.get(eventUtils.COMMENT_CREATE))(comment), - ); - } finally { - eventUtils.setGroup(existingGroup); - } - } - } - - /** - * Decode an XML comment tag and create a comment on the workspace. - * - * @param xmlComment XML comment element. - * @param workspace The workspace. - * @returns The created workspace comment. - * @internal - */ - static fromXml(xmlComment: Element, workspace: Workspace): WorkspaceComment { - const info = WorkspaceComment.parseAttributes(xmlComment); - - const comment = new WorkspaceComment( - workspace, - info.content, - info.h, - info.w, - info.id, - ); - - const xmlX = xmlComment.getAttribute('x'); - const xmlY = xmlComment.getAttribute('y'); - const commentX = xmlX ? parseInt(xmlX, 10) : NaN; - const commentY = xmlY ? parseInt(xmlY, 10) : NaN; - if (!isNaN(commentX) && !isNaN(commentY)) { - comment.moveBy(commentX, commentY); - } - - WorkspaceComment.fireCreateEvent(comment); - return comment; - } - - /** - * Decode an XML comment tag and return the results in an object. - * - * @param xml XML comment element. - * @returns An object containing the id, size, position, and comment string. - * @internal - */ - static parseAttributes(xml: Element): { - id: string; - w: number; - h: number; - x: number; - y: number; - content: string; - } { - const xmlH = xml.getAttribute('h'); - const xmlW = xml.getAttribute('w'); - const xmlX = xml.getAttribute('x'); - const xmlY = xml.getAttribute('y'); - const xmlId = xml.getAttribute('id'); - - if (!xmlId) { - throw new Error('No ID present in XML comment definition.'); - } - - return { - id: xmlId, - // The height of the comment in workspace units, or 100 if not specified. - h: xmlH ? parseInt(xmlH) : 100, - // The width of the comment in workspace units, or 100 if not specified. - w: xmlW ? parseInt(xmlW) : 100, - // The x position of the comment in workspace coordinates, or NaN if not - // specified in the XML. - x: xmlX ? parseInt(xmlX) : NaN, - // The y position of the comment in workspace coordinates, or NaN if not - // specified in the XML. - y: xmlY ? parseInt(xmlY) : NaN, - content: xml.textContent ?? '', - }; - } -} diff --git a/core/workspace_comment_svg.ts b/core/workspace_comment_svg.ts deleted file mode 100644 index 0aa4292c6..000000000 --- a/core/workspace_comment_svg.ts +++ /dev/null @@ -1,1153 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Object representing a code comment on a rendered workspace. - * - * @class - */ -// Former goog.module ID: Blockly.WorkspaceCommentSvg - -// Unused import preserved for side-effects. Remove if unneeded. -import './events/events_selected.js'; - -import * as browserEvents from './browser_events.js'; -import * as common from './common.js'; -// import * as ContextMenu from './contextmenu.js'; -import * as Css from './css.js'; -import type {CommentMove} from './events/events_comment_move.js'; -import * as eventUtils from './events/utils.js'; -import type {IBoundedElement} from './interfaces/i_bounded_element.js'; -import type {IBubble} from './interfaces/i_bubble.js'; -import type {ICopyable} from './interfaces/i_copyable.js'; -import * as Touch from './touch.js'; -import {Coordinate} from './utils/coordinate.js'; -import * as dom from './utils/dom.js'; -import {Rect} from './utils/rect.js'; -import {Svg} from './utils/svg.js'; -import * as svgMath from './utils/svg_math.js'; -import {WorkspaceComment} from './workspace_comment.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; -import { - WorkspaceCommentCopyData, - WorkspaceCommentPaster, -} from './clipboard/workspace_comment_paster.js'; - -/** Size of the resize icon. */ -const RESIZE_SIZE = 8; - -/** Radius of the border around the comment. */ -const BORDER_RADIUS = 3; - -/** Offset from the foreignobject edge to the textarea edge. */ -const TEXTAREA_OFFSET = 2; - -/** - * Class for a workspace comment's SVG representation. - */ -export class WorkspaceCommentSvg - extends WorkspaceComment - implements IBoundedElement, IBubble, ICopyable -{ - /** - * The width and height to use to size a workspace comment when it is first - * added, before it has been edited by the user. - * - * @internal - */ - static DEFAULT_SIZE = 100; - - /** Offset from the top to make room for a top bar. */ - private static readonly TOP_OFFSET = 10; - override workspace: WorkspaceSvg; - - /** Mouse up event data. */ - private onMouseUpWrapper: browserEvents.Data | null = null; - - /** Mouse move event data. */ - private onMouseMoveWrapper: browserEvents.Data | null = null; - - /** Whether event handlers have been initialized. */ - private eventsInit = false; - private textarea: HTMLTextAreaElement | null = null; - - private svgRectTarget: SVGRectElement | null = null; - - private svgHandleTarget: SVGRectElement | null = null; - - private foreignObject: SVGForeignObjectElement | null = null; - - private resizeGroup: SVGGElement | null = null; - - private deleteGroup: SVGGElement | null = null; - - private deleteIconBorder: SVGCircleElement | null = null; - - private focused = false; - private autoLayout = false; - - // Create core elements for the block. - private readonly svgGroup: SVGElement; - svgRect_: SVGRectElement; - - /** Whether the comment is rendered onscreen and is a part of the DOM. */ - private rendered = false; - - /** - * @param workspace The block's workspace. - * @param content The content of this workspace comment. - * @param height Height of the comment. - * @param width Width of the comment. - * @param opt_id Optional ID. Use this ID if provided, otherwise create a new - * ID. - */ - constructor( - workspace: WorkspaceSvg, - content: string, - height: number, - width: number, - opt_id?: string, - ) { - super(workspace, content, height, width, opt_id); - this.svgGroup = dom.createSvgElement(Svg.G, {'class': 'blocklyComment'}); - this.workspace = workspace; - - this.svgRect_ = dom.createSvgElement(Svg.RECT, { - 'class': 'blocklyCommentRect', - 'x': 0, - 'y': 0, - 'rx': BORDER_RADIUS, - 'ry': BORDER_RADIUS, - }); - this.svgGroup.appendChild(this.svgRect_); - - this.render(); - } - - /** - * Dispose of this comment. - * - * @internal - */ - override dispose() { - if (this.disposed_) { - return; - } - // If this comment is being dragged, unlink the mouse events. - if (common.getSelected() === this) { - this.unselect(); - this.workspace.cancelCurrentGesture(); - } - - if (eventUtils.isEnabled()) { - eventUtils.fire(new (eventUtils.get(eventUtils.COMMENT_DELETE))(this)); - } - - dom.removeNode(this.svgGroup); - - eventUtils.disable(); - super.dispose(); - eventUtils.enable(); - } - - /** - * Create and initialize the SVG representation of a workspace comment. - * May be called more than once. - * - * @param opt_noSelect Text inside text area will be selected if false - * @internal - */ - initSvg(opt_noSelect?: boolean) { - if (!this.workspace.rendered) { - throw TypeError('Workspace is headless.'); - } - if (!this.workspace.options.readOnly && !this.eventsInit) { - browserEvents.conditionalBind( - this.svgRectTarget as SVGRectElement, - 'pointerdown', - this, - this.pathMouseDown, - ); - browserEvents.conditionalBind( - this.svgHandleTarget as SVGRectElement, - 'pointerdown', - this, - this.pathMouseDown, - ); - } - this.eventsInit = true; - - this.updateMovable(); - if (!this.getSvgRoot().parentNode) { - this.workspace.getBubbleCanvas().appendChild(this.getSvgRoot()); - } - - if (!opt_noSelect && this.textarea) { - this.textarea.select(); - } - } - - /** - * Handle a pointerdown on an SVG comment. - * - * @param e Pointer down event. - */ - private pathMouseDown(e: PointerEvent) { - const gesture = this.workspace.getGesture(e); - if (gesture) { - gesture.handleBubbleStart(e, this); - } - } - - /** - * Show the context menu for this workspace comment. - * - * @param e Pointer event. - * @internal - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - showContextMenu(e: PointerEvent) { - throw new Error( - 'The implementation of showContextMenu should be ' + - 'monkey-patched in by blockly.ts', - ); - } - - /** - * Select this comment. Highlight it visually. - * - * @internal - */ - select() { - 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(); - } - - /** - * Unselect this comment. Remove its highlighting. - * - * @internal - */ - unselect() { - if (common.getSelected() !== this) { - return; - } - const event = new (eventUtils.get(eventUtils.SELECTED))( - this.id, - null, - this.workspace.id, - ); - eventUtils.fire(event); - common.setSelected(null); - this.removeSelect(); - this.blurFocus(); - } - - /** - * Select this comment. Highlight it visually. - * - * @internal - */ - addSelect() { - dom.addClass(this.svgGroup, 'blocklySelected'); - this.setFocus(); - } - - /** - * Unselect this comment. Remove its highlighting. - * - * @internal - */ - removeSelect() { - dom.addClass(this.svgGroup, 'blocklySelected'); - this.blurFocus(); - } - - /** - * Focus this comment. Highlight it visually. - * - * @internal - */ - addFocus() { - dom.addClass(this.svgGroup, 'blocklyFocused'); - } - - /** - * Unfocus this comment. Remove its highlighting. - * - * @internal - */ - removeFocus() { - dom.removeClass(this.svgGroup, 'blocklyFocused'); - } - - /** - * Return the coordinates of the top-left corner of this comment relative to - * the drawing surface's origin (0,0), in workspace units. - * If the comment is on the workspace, (0, 0) is the origin of the workspace - * coordinate system. - * This does not change with workspace scale. - * - * @returns Object with .x and .y properties in workspace coordinates. - * @internal - */ - override getRelativeToSurfaceXY(): Coordinate { - const layerManger = this.workspace.getLayerManager(); - if (!layerManger) { - throw new Error( - 'Cannot calculate position because the workspace has not been appended', - ); - } - - let x = 0; - let y = 0; - - let element: SVGElement | null = this.getSvgRoot(); - if (element) { - do { - // Loop through this comment and every parent. - const xy = svgMath.getRelativeXY(element); - x += xy.x; - y += xy.y; - element = element.parentNode as SVGElement; - } while (element && !layerManger.hasLayer(element) && element !== null); - } - this.xy_ = new Coordinate(x, y); - return this.xy_; - } - - /** - * Move a comment by a relative offset. - * - * @param dx Horizontal offset, in workspace units. - * @param dy Vertical offset, in workspace units. - * @internal - */ - override moveBy(dx: number, dy: number) { - const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))( - this, - ) as CommentMove; - // TODO: Do I need to look up the relative to surface XY position here? - const xy = this.getRelativeToSurfaceXY(); - this.translate(xy.x + dx, xy.y + dy); - this.xy_ = new Coordinate(xy.x + dx, xy.y + dy); - event.recordNew(); - eventUtils.fire(event); - this.workspace.resizeContents(); - } - - /** - * Transforms a comment by setting the translation on the transform attribute - * of the block's SVG. - * - * @param x The x coordinate of the translation in workspace units. - * @param y The y coordinate of the translation in workspace units. - * @internal - */ - translate(x: number, y: number) { - this.xy_ = new Coordinate(x, y); - this.getSvgRoot().setAttribute( - 'transform', - 'translate(' + x + ',' + y + ')', - ); - } - - /** - * Move this comment during a drag. - * - * @param newLoc The location to translate to, in workspace coordinates. - * @internal - */ - moveDuringDrag(newLoc: Coordinate) { - const translation = `translate(${newLoc.x}, ${newLoc.y})`; - this.getSvgRoot().setAttribute('transform', translation); - } - - /** - * Move the bubble group to the specified location in workspace coordinates. - * - * @param x The x position to move to. - * @param y The y position to move to. - * @internal - */ - moveTo(x: number, y: number) { - this.translate(x, y); - } - - /** - * Clear the comment of transform="..." attributes. - * Used when the comment is switching from 3d to 2d transform or vice versa. - */ - private clearTransformAttributes() { - this.getSvgRoot().removeAttribute('transform'); - } - - /** - * Returns the coordinates of a bounding box describing the dimensions of this - * comment. - * Coordinate system: workspace coordinates. - * - * @returns Object with coordinates of the bounding box. - * @internal - */ - getBoundingRectangle(): Rect { - const blockXY = this.getRelativeToSurfaceXY(); - const commentBounds = this.getHeightWidth(); - const top = blockXY.y; - const bottom = blockXY.y + commentBounds.height; - let left; - let right; - if (this.RTL) { - left = blockXY.x - commentBounds.width; - // Add the width of the tab/puzzle piece knob to the x coordinate - // since X is the corner of the rectangle, not the whole puzzle piece. - right = blockXY.x; - } else { - // Subtract the width of the tab/puzzle piece knob to the x coordinate - // since X is the corner of the rectangle, not the whole puzzle piece. - left = blockXY.x; - right = blockXY.x + commentBounds.width; - } - return new Rect(top, bottom, left, right); - } - - /** - * Add or remove the UI indicating if this comment is movable or not. - * - * @internal - */ - updateMovable() { - if (this.isMovable()) { - dom.addClass(this.svgGroup, 'blocklyDraggable'); - } else { - dom.removeClass(this.svgGroup, 'blocklyDraggable'); - } - } - - /** - * Set whether this comment is movable or not. - * - * @param movable True if movable. - * @internal - */ - override setMovable(movable: boolean) { - super.setMovable(movable); - this.updateMovable(); - } - - /** - * Set whether this comment is editable or not. - * - * @param editable True if editable. - */ - override setEditable(editable: boolean) { - super.setEditable(editable); - if (this.textarea) { - this.textarea.readOnly = !editable; - } - } - - /** - * Recursively adds or removes the dragging class to this node and its - * children. - * - * @param adding True if adding, false if removing. - * @internal - */ - setDragging(adding: boolean) { - if (adding) { - dom.addClass(this.getSvgRoot(), 'blocklyDragging'); - } else { - dom.removeClass(this.getSvgRoot(), 'blocklyDragging'); - } - } - - /** - * Return the root node of the SVG or null if none exists. - * - * @returns The root SVG node (probably a group). - * @internal - */ - getSvgRoot(): SVGElement { - return this.svgGroup; - } - - /** - * Returns this comment's text. - * - * @returns Comment text. - * @internal - */ - override getContent(): string { - return this.textarea ? this.textarea.value : this.content_; - } - - /** - * Set this comment's content. - * - * @param content Comment content. - * @internal - */ - override setContent(content: string) { - super.setContent(content); - if (this.textarea) { - this.textarea.value = content; - } - } - - /** - * Update the cursor over this comment by adding or removing a class. - * - * @param enable True if the delete cursor should be shown, false otherwise. - * @internal - */ - setDeleteStyle(enable: boolean) { - if (enable) { - dom.addClass(this.svgGroup, 'blocklyDraggingDelete'); - } else { - dom.removeClass(this.svgGroup, 'blocklyDraggingDelete'); - } - } - - /** - * Set whether auto-layout of this bubble is enabled. The first time a bubble - * is shown it positions itself to not cover any blocks. Once a user has - * dragged it to reposition, it renders where the user put it. - * - * @param _enable True if auto-layout should be enabled, false otherwise. - * @internal - */ - setAutoLayout(_enable: boolean) {} - // NOP for compatibility with the bubble dragger. - - /** - * Encode a comment subtree as XML with XY coordinates. - * - * @param opt_noId True if the encoder should skip the comment ID. - * @returns Tree of XML elements. - * @internal - */ - override toXmlWithXY(opt_noId?: boolean): Element { - let width = 0; // Not used in LTR. - if (this.workspace.RTL) { - // Here be performance dragons: This calls getMetrics(). - width = this.workspace.getWidth(); - } - const element = this.toXml(opt_noId); - const xy = this.getRelativeToSurfaceXY(); - element.setAttribute( - 'x', - String(Math.round(this.workspace.RTL ? width - xy.x : xy.x)), - ); - element.setAttribute('y', String(Math.round(xy.y))); - element.setAttribute('h', String(this.getHeight())); - element.setAttribute('w', String(this.getWidth())); - return element; - } - - /** - * Encode a comment for copying. - * - * @returns Copy metadata. - */ - toCopyData(): WorkspaceCommentCopyData { - return { - paster: WorkspaceCommentPaster.TYPE, - commentState: this.toXmlWithXY(), - }; - } - - /** - * Returns a bounding box describing the dimensions of this comment. - * - * @returns Object with height and width properties in workspace units. - * @internal - */ - getHeightWidth(): {height: number; width: number} { - return {width: this.getWidth(), height: this.getHeight()}; - } - - /** - * Renders the workspace comment. - * - * @internal - */ - render() { - if (this.rendered) { - return; - } - - const size = this.getHeightWidth(); - - // Add text area - const foreignObject = this.createEditor(); - this.svgGroup.appendChild(foreignObject); - - this.svgHandleTarget = dom.createSvgElement(Svg.RECT, { - 'class': 'blocklyCommentHandleTarget', - 'x': 0, - 'y': 0, - }); - this.svgGroup.appendChild(this.svgHandleTarget); - this.svgRectTarget = dom.createSvgElement(Svg.RECT, { - 'class': 'blocklyCommentTarget', - 'x': 0, - 'y': 0, - 'rx': BORDER_RADIUS, - 'ry': BORDER_RADIUS, - }); - this.svgGroup.appendChild(this.svgRectTarget); - - // Add the resize icon - this.addResizeDom(); - if (this.isDeletable()) { - // Add the delete icon - this.addDeleteDom(); - } - - this.setSize(size.width, size.height); - - // Set the content - this.textarea!.value = this.content_; - - this.rendered = true; - - if (this.resizeGroup) { - browserEvents.conditionalBind( - this.resizeGroup, - 'pointerdown', - this, - this.resizeMouseDown, - ); - } - - if (this.isDeletable()) { - browserEvents.conditionalBind( - this.deleteGroup as SVGGElement, - 'pointerdown', - this, - this.deleteMouseDown, - ); - browserEvents.conditionalBind( - this.deleteGroup as SVGGElement, - 'pointerout', - this, - this.deleteMouseOut, - ); - browserEvents.conditionalBind( - this.deleteGroup as SVGGElement, - 'pointerup', - this, - this.deleteMouseUp, - ); - } - } - - /** - * Create the text area for the comment. - * - * @returns The top-level node of the editor. - */ - private createEditor(): Element { - /* Create the editor. Here's the markup that will be generated: - - - - */ - this.foreignObject = dom.createSvgElement(Svg.FOREIGNOBJECT, { - 'x': 0, - 'y': WorkspaceCommentSvg.TOP_OFFSET, - 'class': 'blocklyCommentForeignObject', - }); - 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; - textarea.className = 'blocklyCommentTextarea'; - textarea.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); - textarea.readOnly = !this.isEditable(); - body.appendChild(textarea); - this.textarea = textarea; - this.foreignObject.appendChild(body); - // Don't zoom with mousewheel. - browserEvents.conditionalBind( - textarea, - 'wheel', - this, - function (e: WheelEvent) { - e.stopPropagation(); - }, - ); - browserEvents.conditionalBind( - textarea, - 'change', - this, - function (this: WorkspaceCommentSvg, _e: Event) { - this.setContent(textarea.value); - }, - ); - return this.foreignObject; - } - - /** Add the resize icon to the DOM */ - private addResizeDom() { - this.resizeGroup = dom.createSvgElement( - Svg.G, - {'class': this.RTL ? 'blocklyResizeSW' : 'blocklyResizeSE'}, - this.svgGroup, - ); - dom.createSvgElement( - Svg.POLYGON, - { - 'points': `0,${RESIZE_SIZE} ${RESIZE_SIZE},${RESIZE_SIZE} ${RESIZE_SIZE},0`, - }, - this.resizeGroup, - ); - dom.createSvgElement( - Svg.LINE, - { - 'class': 'blocklyResizeLine', - 'x1': RESIZE_SIZE / 3, - 'y1': RESIZE_SIZE - 1, - 'x2': RESIZE_SIZE - 1, - 'y2': RESIZE_SIZE / 3, - }, - this.resizeGroup, - ); - dom.createSvgElement( - Svg.LINE, - { - 'class': 'blocklyResizeLine', - 'x1': (RESIZE_SIZE * 2) / 3, - 'y1': RESIZE_SIZE - 1, - 'x2': RESIZE_SIZE - 1, - 'y2': (RESIZE_SIZE * 2) / 3, - }, - this.resizeGroup, - ); - } - - /** Add the delete icon to the DOM */ - private addDeleteDom() { - this.deleteGroup = dom.createSvgElement( - Svg.G, - {'class': 'blocklyCommentDeleteIcon'}, - this.svgGroup, - ); - this.deleteIconBorder = dom.createSvgElement( - Svg.CIRCLE, - {'class': 'blocklyDeleteIconShape', 'r': '7', 'cx': '7.5', 'cy': '7.5'}, - this.deleteGroup, - ); - // x icon. - dom.createSvgElement( - Svg.LINE, - { - 'x1': '5', - 'y1': '10', - 'x2': '10', - 'y2': '5', - 'stroke': '#fff', - 'stroke-width': '2', - }, - this.deleteGroup, - ); - dom.createSvgElement( - Svg.LINE, - { - 'x1': '5', - 'y1': '5', - 'x2': '10', - 'y2': '10', - 'stroke': '#fff', - 'stroke-width': '2', - }, - this.deleteGroup, - ); - } - - /** - * Handle a pointerdown on comment's resize corner. - * - * @param e Pointer down event. - */ - private resizeMouseDown(e: PointerEvent) { - this.unbindDragEvents(); - if (browserEvents.isRightButton(e)) { - // No right-click. - e.stopPropagation(); - return; - } - // Left-click (or middle click) - this.workspace.startDrag( - e, - new Coordinate( - this.workspace.RTL ? -this.width_ : this.width_, - this.height_, - ), - ); - - this.onMouseUpWrapper = browserEvents.conditionalBind( - document, - 'pointerup', - this, - this.resizeMouseUp, - ); - this.onMouseMoveWrapper = browserEvents.conditionalBind( - document, - 'pointermove', - this, - this.resizeMouseMove, - ); - this.workspace.hideChaff(); - // This event has been handled. No need to bubble up to the document. - e.stopPropagation(); - } - - /** - * Handle a pointerdown on comment's delete icon. - * - * @param e Pointer down event. - */ - private deleteMouseDown(e: PointerEvent) { - // Highlight the delete icon. - if (this.deleteIconBorder) { - dom.addClass(this.deleteIconBorder, 'blocklyDeleteIconHighlighted'); - } - // This event has been handled. No need to bubble up to the document. - e.stopPropagation(); - } - - /** - * Handle a pointerout on comment's delete icon. - * - * @param _e Pointer out event. - */ - private deleteMouseOut(_e: PointerEvent) { - // Restore highlight on the delete icon. - if (this.deleteIconBorder) { - dom.removeClass(this.deleteIconBorder, 'blocklyDeleteIconHighlighted'); - } - } - - /** - * Handle a pointerup on comment's delete icon. - * - * @param e Pointer up event. - */ - private deleteMouseUp(e: PointerEvent) { - // Delete this comment. - this.dispose(); - // This event has been handled. No need to bubble up to the document. - e.stopPropagation(); - } - - /** Stop binding to the global pointerup and pointermove events. */ - private unbindDragEvents() { - if (this.onMouseUpWrapper) { - browserEvents.unbind(this.onMouseUpWrapper); - this.onMouseUpWrapper = null; - } - if (this.onMouseMoveWrapper) { - browserEvents.unbind(this.onMouseMoveWrapper); - this.onMouseMoveWrapper = null; - } - } - - /** - * Handle a pointerup event while dragging a comment's border or resize - * handle. - * - * @param _e Pointer up event. - */ - private resizeMouseUp(_e: PointerEvent) { - Touch.clearTouchIdentifier(); - this.unbindDragEvents(); - } - - /** - * Resize this comment to follow the pointer. - * - * @param e Pointer move event. - */ - private resizeMouseMove(e: PointerEvent) { - this.autoLayout = false; - const newXY = this.workspace.moveDrag(e); - this.setSize(this.RTL ? -newXY.x : newXY.x, newXY.y); - } - - /** - * Callback function triggered when the comment has resized. - * Resize the text area accordingly. - */ - private resizeComment() { - const size = this.getHeightWidth(); - const topOffset = WorkspaceCommentSvg.TOP_OFFSET; - const textOffset = TEXTAREA_OFFSET * 2; - - this.foreignObject?.setAttribute('width', String(size.width)); - this.foreignObject?.setAttribute('height', String(size.height - topOffset)); - if (this.RTL) { - this.foreignObject?.setAttribute('x', String(-size.width)); - } - - if (!this.textarea) return; - this.textarea.style.width = size.width - textOffset + 'px'; - this.textarea.style.height = size.height - textOffset - topOffset + 'px'; - } - - /** - * Set size - * - * @param width width of the container - * @param height height of the container - */ - private setSize(width: number, height: number) { - // Minimum size of a comment. - width = Math.max(width, 45); - height = Math.max(height, 20 + WorkspaceCommentSvg.TOP_OFFSET); - this.width_ = width; - this.height_ = height; - this.svgRect_.setAttribute('width', `${width}`); - this.svgRect_.setAttribute('height', `${height}`); - this.svgRectTarget?.setAttribute('width', `${width}`); - this.svgRectTarget?.setAttribute('height', `${height}`); - this.svgHandleTarget?.setAttribute('width', `${width}`); - this.svgHandleTarget?.setAttribute( - 'height', - String(WorkspaceCommentSvg.TOP_OFFSET), - ); - if (this.RTL) { - this.svgRect_.setAttribute('transform', 'scale(-1 1)'); - this.svgRectTarget?.setAttribute('transform', 'scale(-1 1)'); - } - - if (this.resizeGroup) { - if (this.RTL) { - // Mirror the resize group. - this.resizeGroup.setAttribute( - 'transform', - 'translate(' + - (-width + RESIZE_SIZE) + - ',' + - (height - RESIZE_SIZE) + - ') scale(-1 1)', - ); - this.deleteGroup?.setAttribute( - 'transform', - 'translate(' + - (-width + RESIZE_SIZE) + - ',' + - -RESIZE_SIZE + - ') scale(-1 1)', - ); - } else { - this.resizeGroup.setAttribute( - 'transform', - 'translate(' + - (width - RESIZE_SIZE) + - ',' + - (height - RESIZE_SIZE) + - ')', - ); - this.deleteGroup?.setAttribute( - 'transform', - 'translate(' + (width - RESIZE_SIZE) + ',' + -RESIZE_SIZE + ')', - ); - } - } - - // Allow the contents to resize. - this.resizeComment(); - } - - /** - * Set the focus on the text area. - * - * @internal - */ - setFocus() { - this.focused = true; - // Defer CSS changes. - setTimeout(() => { - if (this.disposed_) { - return; - } - this.textarea!.focus(); - this.addFocus(); - if (this.svgRectTarget) { - dom.addClass(this.svgRectTarget, 'blocklyCommentTargetFocused'); - } - if (this.svgHandleTarget) { - dom.addClass(this.svgHandleTarget, 'blocklyCommentHandleTargetFocused'); - } - }, 0); - } - - /** - * Remove focus from the text area. - * - * @internal - */ - blurFocus() { - this.focused = false; - // Defer CSS changes. - setTimeout(() => { - if (this.disposed_) { - return; - } - - this.textarea!.blur(); - this.removeFocus(); - if (this.svgRectTarget) { - dom.removeClass(this.svgRectTarget, 'blocklyCommentTargetFocused'); - } - if (this.svgHandleTarget) { - dom.removeClass( - this.svgHandleTarget, - 'blocklyCommentHandleTargetFocused', - ); - } - }, 0); - } - - /** - * Decode an XML comment tag and create a rendered comment on the workspace. - * - * @param xmlComment XML comment element. - * @param workspace The workspace. - * @param opt_wsWidth The width of the workspace, which is used to position - * comments correctly in RTL. - * @returns The created workspace comment. - * @internal - */ - static fromXmlRendered( - xmlComment: Element, - workspace: WorkspaceSvg, - opt_wsWidth?: number, - ): WorkspaceCommentSvg { - eventUtils.disable(); - let comment; - try { - const info = WorkspaceComment.parseAttributes(xmlComment); - - comment = new WorkspaceCommentSvg( - workspace, - info.content, - info.h, - info.w, - info.id, - ); - if (workspace.rendered) { - comment.initSvg(true); - comment.render(); - } - // Position the comment correctly, taking into account the width of a - // rendered RTL workspace. - if (!isNaN(info.x) && !isNaN(info.y)) { - if (workspace.RTL) { - const wsWidth = opt_wsWidth || workspace.getWidth(); - comment.moveBy(wsWidth - info.x, info.y); - } else { - comment.moveBy(info.x, info.y); - } - } - } finally { - eventUtils.enable(); - } - - WorkspaceComment.fireCreateEvent(comment); - return comment; - } -} - -/** CSS for workspace comment. See css.js for use. */ -Css.register(` -.blocklyCommentForeignObject { - position: relative; - z-index: 0; -} - -.blocklyCommentRect { - fill: #E7DE8E; - stroke: #bcA903; - stroke-width: 1px; -} - -.blocklyCommentTarget { - fill: transparent; - stroke: #bcA903; -} - -.blocklyCommentTargetFocused { - fill: none; -} - -.blocklyCommentHandleTarget { - fill: none; -} - -.blocklyCommentHandleTargetFocused { - fill: transparent; -} - -.blocklyFocused>.blocklyCommentRect { - fill: #B9B272; - stroke: #B9B272; -} - -.blocklySelected>.blocklyCommentTarget { - stroke: #fc3; - stroke-width: 3px; -} - -.blocklyCommentDeleteIcon { - cursor: pointer; - fill: #000; - display: none; -} - -.blocklySelected > .blocklyCommentDeleteIcon { - display: block; -} - -.blocklyDeleteIconShape { - fill: #000; - stroke: #000; - stroke-width: 1px; -} - -.blocklyDeleteIconShape.blocklyDeleteIconHighlighted { - stroke: #fc3; -} -`); diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 78d083f9b..14cc1101f 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -24,7 +24,6 @@ import type {BlocklyOptions} from './blockly_options.js'; import * as browserEvents from './browser_events.js'; import * as common from './common.js'; import {ComponentManager} from './component_manager.js'; -import {config} from './config.js'; import {ConnectionDB} from './connection_db.js'; import * as ContextMenu from './contextmenu.js'; import {ContextMenuRegistry} from './contextmenu_registry.js'; @@ -35,7 +34,6 @@ import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; -import type {ICopyData, ICopyable} from './interfaces/i_copyable.js'; import type {IDragTarget} from './interfaces/i_drag_target.js'; import type {IFlyout} from './interfaces/i_flyout.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; @@ -49,7 +47,6 @@ import * as registry from './registry.js'; import * as blockRendering from './renderers/common/block_rendering.js'; import type {Renderer} from './renderers/common/renderer.js'; import type {ScrollbarPair} from './scrollbar_pair.js'; -import * as blocks from './serialization/blocks.js'; import type {Theme} from './theme.js'; import {Classic} from './theme/classic.js'; import {ThemeManager} from './theme_manager.js'; @@ -71,14 +68,12 @@ 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 * as Xml from './xml.js'; +import {WorkspaceComment} from './comments/workspace_comment.js'; import {ZoomControls} from './zoom_controls.js'; import {ContextMenuOption} from './contextmenu_registry.js'; import * as renderManagement from './render_management.js'; -import * as deprecation from './utils/deprecation.js'; import {LayerManager} from './layer_manager.js'; +import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; /** Margin around the top/bottom/left/right after a zoomToFit call. */ const ZOOM_TO_FIT_MARGIN = 20; @@ -1094,8 +1089,6 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** * @returns The layer manager for this workspace. - * - * @internal */ getLayerManager(): LayerManager | null { return this.layerManager; @@ -1269,12 +1262,10 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { blocks[i].queueRender(); } - if (this.currentGesture_) { - const imList = this.currentGesture_.getInsertionMarkers(); - for (let i = 0; i < imList.length; i++) { - imList[i].queueRender(); - } - } + this.getTopBlocks() + .flatMap((block) => block.getDescendants(false)) + .filter((block) => block.isInsertionMarker()) + .forEach((block) => block.queueRender()); renderManagement .finishQueuedRenders() @@ -1313,183 +1304,6 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { } } - /** - * Pastes the provided block or workspace comment onto the workspace. - * Does not check whether there is remaining capacity for the object, that - * should be done before calling this method. - * - * @param state The representation of the thing to paste. - * @returns The pasted thing, or null if the paste was not successful. - * @deprecated v10. Use `Blockly.clipboard.paste` instead. To be removed in - * v11. - */ - paste( - state: AnyDuringMigration | Element | DocumentFragment, - ): ICopyable | null { - deprecation.warn( - 'Blockly.WorkspaceSvg.prototype.paste', - 'v10', - 'v11', - 'Blockly.clipboard.paste', - ); - if (!this.rendered || (!state['type'] && !state['tagName'])) { - return null; - } - if (this.currentGesture_) { - // Dragging while pasting? No. - this.currentGesture_.cancel(); - } - - const existingGroup = eventUtils.getGroup(); - if (!existingGroup) { - eventUtils.setGroup(true); - } - let pastedThing; - try { - // Checks if this is JSON. JSON has a type property, while elements don't. - if (state['type']) { - pastedThing = this.pasteBlock_(null, state as blocks.State); - } else { - const xmlBlock = state as Element; - if (xmlBlock.tagName.toLowerCase() === 'comment') { - pastedThing = this.pasteWorkspaceComment_(xmlBlock); - } else { - pastedThing = this.pasteBlock_(xmlBlock, null); - } - } - } finally { - eventUtils.setGroup(existingGroup); - } - return pastedThing; - } - - /** - * Paste the provided block onto the workspace. - * - * @param xmlBlock XML block element. - * @param jsonBlock JSON block representation. - * @returns The pasted block. - */ - private pasteBlock_( - xmlBlock: Element | null, - jsonBlock: blocks.State | null, - ): BlockSvg { - eventUtils.disable(); - let block: BlockSvg; - try { - let blockX = 0; - let blockY = 0; - if (xmlBlock) { - block = Xml.domToBlockInternal(xmlBlock, this) as BlockSvg; - blockX = parseInt(xmlBlock.getAttribute('x') ?? '0'); - if (this.RTL) { - blockX = -blockX; - } - blockY = parseInt(xmlBlock.getAttribute('y') ?? '0'); - } else if (jsonBlock) { - block = blocks.append(jsonBlock, this) as BlockSvg; - blockX = jsonBlock['x'] || 10; - if (this.RTL) { - blockX = this.getWidth() - blockX; - } - blockY = jsonBlock['y'] || 10; - } - - // Move the duplicate to original position. - if (!isNaN(blockX) && !isNaN(blockY)) { - // Offset block until not clobbering another block and not in connection - // distance with neighbouring blocks. - let collide; - do { - collide = false; - const allBlocks = this.getAllBlocks(false); - for (let i = 0, otherBlock; (otherBlock = allBlocks[i]); i++) { - const otherXY = otherBlock.getRelativeToSurfaceXY(); - if ( - Math.abs(blockX - otherXY.x) <= 1 && - Math.abs(blockY - otherXY.y) <= 1 - ) { - collide = true; - break; - } - } - if (!collide) { - // Check for blocks in snap range to any of its connections. - const connections = block!.getConnections_(false); - for (let i = 0, connection; (connection = connections[i]); i++) { - const neighbour = connection.closest( - config.snapRadius, - // This code doesn't work because it's passing absolute coords - // instead of relative coords. But we're deprecating the `paste` - // function anyway so we're not going to fix it. - new Coordinate(blockX, blockY), - ); - if (neighbour.connection) { - collide = true; - break; - } - } - } - if (collide) { - if (this.RTL) { - blockX -= config.snapRadius; - } else { - blockX += config.snapRadius; - } - blockY += config.snapRadius * 2; - } - } while (collide); - // No 'reason' provided since events are disabled. - block!.moveTo(new Coordinate(blockX, blockY)); - } - } finally { - eventUtils.enable(); - } - if (eventUtils.isEnabled() && !block!.isShadow()) { - eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block!)); - } - block!.select(); - return block!; - } - - /** - * Paste the provided comment onto the workspace. - * - * @param xmlComment XML workspace comment element. - * @returns The pasted workspace comment. - */ - private pasteWorkspaceComment_(xmlComment: Element): WorkspaceCommentSvg { - eventUtils.disable(); - let comment: WorkspaceCommentSvg; - try { - comment = WorkspaceCommentSvg.fromXmlRendered(xmlComment, this); - // Move the duplicate to original position. - let commentX = parseInt(xmlComment.getAttribute('x') ?? '0'); - let commentY = parseInt(xmlComment.getAttribute('y') ?? '0'); - if (!isNaN(commentX) && !isNaN(commentY)) { - if (this.RTL) { - commentX = -commentX; - } - // Offset workspace comment. - // TODO (#1719): Properly offset comment such that it's not interfering - // with any blocks. - commentX += 50; - commentY += 50; - // This code doesn't work because it's passing absolute coords - // instead of relative coords. But we're deprecating the `paste` - // function anyway so we're not going to fix it. - comment.moveBy(commentX, commentY); - } - } finally { - eventUtils.enable(); - } - if (eventUtils.isEnabled()) { - WorkspaceComment.fireCreateEvent(comment); - } - comment.select(); - return comment; - } - /** * Refresh the toolbox unless there's a drag in progress. * @@ -1581,6 +1395,20 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { '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 */ /** @@ -1844,7 +1672,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @param e Mouse event. * @internal */ - showContextMenu(e: Event) { + showContextMenu(e: PointerEvent) { if (this.options.readOnly || this.isFlyout) { return; } @@ -2315,7 +2143,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @param comment comment to add. */ override addTopComment(comment: WorkspaceComment) { - this.addTopBoundedElement(comment as WorkspaceCommentSvg); + this.addTopBoundedElement(comment as RenderedWorkspaceComment); super.addTopComment(comment); } @@ -2325,7 +2153,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @param comment comment to remove. */ override removeTopComment(comment: WorkspaceComment) { - this.removeTopBoundedElement(comment as WorkspaceCommentSvg); + this.removeTopBoundedElement(comment as RenderedWorkspaceComment); super.removeTopComment(comment); } diff --git a/core/xml.ts b/core/xml.ts index db75e04c9..686c71082 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -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 type {Field} from './field.js'; import {IconType} from './icons/icon_types.js'; @@ -19,22 +21,21 @@ import * as utilsXml from './utils/xml.js'; import type {VariableModel} from './variable_model.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; -import {WorkspaceComment} from './workspace_comment.js'; -import {WorkspaceCommentSvg} from './workspace_comment_svg.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; +import {WorkspaceSvg} from './workspace_svg.js'; import * as renderManagement from './render_management.js'; +import {WorkspaceComment} from './comments/workspace_comment.js'; +import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; +import {Coordinate} from './utils/coordinate.js'; /** * Encode a block tree as XML. * * @param workspace The workspace containing blocks. - * @param opt_noId True if the encoder should skip the block IDs. + * @param skipId True if the encoder should skip the block IDs. False by + * default. * @returns XML DOM element. */ -export function workspaceToDom( - workspace: Workspace, - opt_noId?: boolean, -): Element { +export function workspaceToDom(workspace: Workspace, skipId = false): Element { const treeXml = utilsXml.createElement('xml'); const variablesElement = variablesToDom( Variables.allUsedVarModels(workspace), @@ -42,19 +43,44 @@ export function workspaceToDom( if (variablesElement.hasChildNodes()) { treeXml.appendChild(variablesElement); } - const comments = workspace.getTopComments(true); - for (let i = 0; i < comments.length; i++) { - const comment = comments[i]; - treeXml.appendChild(comment.toXmlWithXY(opt_noId)); + for (const comment of workspace.getTopComments()) { + treeXml.appendChild( + saveWorkspaceComment(comment as AnyDuringMigration, skipId), + ); } const blocks = workspace.getTopBlocks(true); for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; - treeXml.appendChild(blockToDomWithXY(block, opt_noId)); + treeXml.appendChild(blockToDomWithXY(block, skipId)); } return treeXml; } +/** Serializes the given workspace comment to XML. */ +export function saveWorkspaceComment( + comment: WorkspaceComment, + skipId = false, +): Element { + const elem = utilsXml.createElement('comment'); + if (!skipId) elem.setAttribute('id', comment.id); + + const workspace = comment.workspace; + const loc = comment.getRelativeToSurfaceXY(); + loc.x = workspace.RTL ? workspace.getWidth() - loc.x : loc.x; + elem.setAttribute('x', `${loc.x}`); + elem.setAttribute('y', `${loc.y}`); + elem.setAttribute('w', `${comment.getSize().width}`); + elem.setAttribute('h', `${comment.getSize().height}`); + + if (comment.getText()) elem.textContent = comment.getText(); + if (comment.isCollapsed()) elem.setAttribute('collapsed', 'true'); + if (!comment.isOwnEditable()) elem.setAttribute('editable', 'false'); + if (!comment.isOwnMovable()) elem.setAttribute('movable', 'false'); + if (!comment.isOwnDeletable()) elem.setAttribute('deletable', 'false'); + + return elem; +} + /** * Encode a list of variables as XML. * @@ -248,15 +274,21 @@ export function blockToDom( element.setAttribute('collapsed', 'true'); } if (!block.isEnabled()) { - element.setAttribute('disabled', 'true'); + // Set the value of the attribute to a comma-separated list of reasons. + // Use encodeURIComponent to escape commas in the reasons so that they + // won't be confused with separator commas. + element.setAttribute( + 'disabled-reasons', + Array.from(block.getDisabledReasons()).map(encodeURIComponent).join(','), + ); } - if (!block.isDeletable() && !block.isShadow()) { + if (!block.isOwnDeletable()) { element.setAttribute('deletable', 'false'); } - if (!block.isMovable() && !block.isShadow()) { + if (!block.isOwnMovable()) { element.setAttribute('movable', 'false'); } - if (!block.isEditable()) { + if (!block.isOwnEditable()) { element.setAttribute('editable', 'false'); } @@ -443,15 +475,7 @@ export function domToWorkspace(xml: Element, workspace: Workspace): string[] { } else if (name === 'shadow') { throw TypeError('Shadow block cannot be a top-level block.'); } else if (name === 'comment') { - if (workspace.rendered) { - WorkspaceCommentSvg.fromXmlRendered( - xmlChildElement, - workspace as WorkspaceSvg, - width, - ); - } else { - WorkspaceComment.fromXml(xmlChildElement, workspace); - } + loadWorkspaceComment(xmlChildElement, workspace); } else if (name === 'variables') { if (variablesFirst) { domToVariables(xmlChildElement, workspace); @@ -478,6 +502,37 @@ export function domToWorkspace(xml: Element, workspace: Workspace): string[] { return newBlockIds; } +/** Deserializes the given comment state into the given workspace. */ +export function loadWorkspaceComment( + elem: Element, + workspace: Workspace, +): WorkspaceComment { + const id = elem.getAttribute('id') ?? undefined; + const comment = workspace.rendered + ? new RenderedWorkspaceComment(workspace as WorkspaceSvg, id) + : new WorkspaceComment(workspace, id); + + comment.setText(elem.textContent ?? ''); + + let x = parseInt(elem.getAttribute('x') ?? '', 10); + const y = parseInt(elem.getAttribute('y') ?? '', 10); + if (!isNaN(x) && !isNaN(y)) { + x = workspace.RTL ? workspace.getWidth() - x : x; + comment.moveTo(new Coordinate(x, y)); + } + + const w = parseInt(elem.getAttribute('w') ?? '', 10); + const h = parseInt(elem.getAttribute('h') ?? '', 10); + if (!isNaN(w) && !isNaN(h)) comment.setSize(new Size(w, h)); + + if (elem.getAttribute('collapsed') === 'true') comment.setCollapsed(true); + if (elem.getAttribute('editable') === 'false') comment.setEditable(false); + if (elem.getAttribute('movable') === 'false') comment.setMovable(false); + if (elem.getAttribute('deletable') === 'false') comment.setDeletable(false); + + return comment; +} + /** * Decode an XML DOM and create blocks on the workspace. Position the new * blocks immediately below prior blocks, aligned by their starting edge. @@ -968,7 +1023,24 @@ function domToBlockHeadless( } const disabled = xmlBlock.getAttribute('disabled'); if (disabled) { - block.setEnabled(disabled !== 'true' && disabled !== 'disabled'); + deprecation.warn( + 'disabled', + 'v11', + 'v12', + 'disabled-reasons with the value "' + MANUALLY_DISABLED + '"', + ); + block.setDisabledReason( + disabled === 'true' || disabled === 'disabled', + MANUALLY_DISABLED, + ); + } + const disabledReasons = xmlBlock.getAttribute('disabled-reasons'); + if (disabledReasons !== null) { + for (const reason of disabledReasons.split(',')) { + // Use decodeURIComponent to restore characters that were encoded in the + // value, such as commas. + block.setDisabledReason(true, decodeURIComponent(reason)); + } } const deletable = xmlBlock.getAttribute('deletable'); if (deletable) { diff --git a/demos/blockfactory/blocks.js b/demos/blockfactory/blocks.js index b93ae5739..15c8ac864 100644 --- a/demos/blockfactory/blocks.js +++ b/demos/blockfactory/blocks.js @@ -880,7 +880,7 @@ function fieldNameCheck(referenceBlock) { var blocks = referenceBlock.workspace.getAllBlocks(false); for (var i = 0, block; block = blocks[i]; i++) { var otherName = block.getFieldValue('FIELDNAME'); - if (!block.disabled && !block.getInheritedDisabled() && + if (block.isEnabled() && !block.getInheritedDisabled() && otherName && otherName.toLowerCase() === name) { count++; } @@ -905,7 +905,7 @@ function inputNameCheck(referenceBlock) { var blocks = referenceBlock.workspace.getAllBlocks(false); for (var i = 0, block; block = blocks[i]; i++) { var otherName = block.getFieldValue('INPUTNAME'); - if (!block.disabled && !block.getInheritedDisabled() && + if (block.isEnabled() && !block.getInheritedDisabled() && otherName && otherName.toLowerCase() === name) { count++; } diff --git a/demos/blockfactory/factory_utils.js b/demos/blockfactory/factory_utils.js index 148498360..4731d1ce9 100644 --- a/demos/blockfactory/factory_utils.js +++ b/demos/blockfactory/factory_utils.js @@ -163,7 +163,7 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) { var contentsBlock = rootBlock.getInputTargetBlock('INPUTS'); var lastInput = null; while (contentsBlock) { - if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { + if (contentsBlock.isEnabled() && !contentsBlock.getInheritedDisabled()) { var fields = FactoryUtils.getFieldsJson_( contentsBlock.getInputTargetBlock('FIELDS')); for (var i = 0; i < fields.length; i++) { @@ -247,7 +247,7 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) { } // Generate colour. var colourBlock = rootBlock.getInputTargetBlock('COLOUR'); - if (colourBlock && !colourBlock.disabled) { + if (colourBlock && colourBlock.isEnabled()) { var hue = parseInt(colourBlock.getFieldValue('HUE'), 10); JS.colour = hue; } @@ -277,7 +277,7 @@ FactoryUtils.formatJavaScript_ = function(blockType, rootBlock, workspace) { 'input_end_row': 'appendEndRowInput'}; var contentsBlock = rootBlock.getInputTargetBlock('INPUTS'); while (contentsBlock) { - if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { + if (contentsBlock.isEnabled() && !contentsBlock.getInheritedDisabled()) { var name = ''; // Dummy inputs don't have names. Other inputs do. if (contentsBlock.type !== 'input_dummy' && @@ -333,7 +333,7 @@ FactoryUtils.formatJavaScript_ = function(blockType, rootBlock, workspace) { } // Generate colour. var colourBlock = rootBlock.getInputTargetBlock('COLOUR'); - if (colourBlock && !colourBlock.disabled) { + if (colourBlock && colourBlock.isEnabled()) { var hue = parseInt(colourBlock.getFieldValue('HUE'), 10); if (!isNaN(hue)) { code.push(' this.setColour(' + hue + ');'); @@ -377,7 +377,7 @@ FactoryUtils.connectionLineJs_ = function(functionName, typeName, workspace) { FactoryUtils.getFieldsJs_ = function(block) { var fields = []; while (block) { - if (!block.disabled && !block.getInheritedDisabled()) { + if (block.isEnabled() && !block.getInheritedDisabled()) { switch (block.type) { case 'field_static': // Result: 'hello' @@ -484,7 +484,7 @@ FactoryUtils.getFieldsJs_ = function(block) { FactoryUtils.getFieldsJson_ = function(block) { var fields = []; while (block) { - if (!block.disabled && !block.getInheritedDisabled()) { + if (block.isEnabled() && !block.getInheritedDisabled()) { switch (block.type) { case 'field_static': // Result: 'hello' @@ -614,7 +614,7 @@ FactoryUtils.getOptTypesFrom = function(block, name) { FactoryUtils.getTypesFrom_ = function(block, name) { var typeBlock = block.getInputTargetBlock(name); var types; - if (!typeBlock || typeBlock.disabled) { + if (!typeBlock || !typeBlock.isEnabled()) { types = []; } else if (typeBlock.type === 'type_other') { types = [JSON.stringify(typeBlock.getFieldValue('TYPE'))]; @@ -1015,7 +1015,7 @@ FactoryUtils.savedBlockChanges = function(blockLibraryController) { */ FactoryUtils.getTooltipFromRootBlock_ = function(rootBlock) { var tooltipBlock = rootBlock.getInputTargetBlock('TOOLTIP'); - if (tooltipBlock && !tooltipBlock.disabled) { + if (tooltipBlock && tooltipBlock.isEnabled()) { return tooltipBlock.getFieldValue('TEXT'); } return ''; @@ -1029,7 +1029,7 @@ FactoryUtils.getTooltipFromRootBlock_ = function(rootBlock) { */ FactoryUtils.getHelpUrlFromRootBlock_ = function(rootBlock) { var helpUrlBlock = rootBlock.getInputTargetBlock('HELPURL'); - if (helpUrlBlock && !helpUrlBlock.disabled) { + if (helpUrlBlock && helpUrlBlock.isEnabled()) { return helpUrlBlock.getFieldValue('TEXT'); } return ''; diff --git a/demos/blockfactory_old/blocks.js b/demos/blockfactory_old/blocks.js deleted file mode 100644 index 2e5fd7ba0..000000000 --- a/demos/blockfactory_old/blocks.js +++ /dev/null @@ -1,794 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Blocks for Blockly's Block Factory application. - */ -'use strict'; - -Blockly.Blocks['factory_base'] = { - // Base of new block. - init: function() { - this.setColour(120); - this.appendDummyInput() - .appendField('name') - .appendField(new Blockly.FieldTextInput('block_type'), 'NAME'); - this.appendStatementInput('INPUTS') - .setCheck('Input') - .appendField('inputs'); - var dropdown = new Blockly.FieldDropdown([ - ['automatic inputs', 'AUTO'], - ['external inputs', 'EXT'], - ['inline inputs', 'INT']]); - this.appendDummyInput() - .appendField(dropdown, 'INLINE'); - dropdown = new Blockly.FieldDropdown([ - ['no connections', 'NONE'], - ['← left output', 'LEFT'], - ['↕ top+bottom connections', 'BOTH'], - ['↑ top connection', 'TOP'], - ['↓ bottom connection', 'BOTTOM']], - function(option) { - this.sourceBlock_.updateShape_(option); - // Connect a shadow block to this new input. - this.sourceBlock_.spawnOutputShadow_(option); - }); - this.appendDummyInput() - .appendField(dropdown, 'CONNECTIONS'); - this.appendValueInput('COLOUR') - .setCheck('Colour') - .appendField('colour'); - this.setTooltip('Build a custom block by plugging\n' + - 'fields, inputs and other blocks here.'); - this.setHelpUrl( - 'https://developers.google.com/blockly/guides/create-custom-blocks/block-factory'); - }, - mutationToDom: function() { - var container = Blockly.utils.xml.createElement('mutation'); - container.setAttribute('connections', this.getFieldValue('CONNECTIONS')); - return container; - }, - domToMutation: function(xmlElement) { - var connections = xmlElement.getAttribute('connections'); - this.updateShape_(connections); - }, - spawnOutputShadow_: function(option) { - // Helper method for deciding which type of outputs this block needs - // to attach shadow blocks to. - switch (option) { - case 'LEFT': - this.connectOutputShadow_('OUTPUTTYPE'); - break; - case 'TOP': - this.connectOutputShadow_('TOPTYPE'); - break; - case 'BOTTOM': - this.connectOutputShadow_('BOTTOMTYPE'); - break; - case 'BOTH': - this.connectOutputShadow_('TOPTYPE'); - this.connectOutputShadow_('BOTTOMTYPE'); - break; - } - }, - connectOutputShadow_: function(outputType) { - // Helper method to create & connect shadow block. - var type = this.workspace.newBlock('type_null'); - type.setShadow(true); - type.outputConnection.connect(this.getInput(outputType).connection); - type.initSvg(); - type.render(); - }, - updateShape_: function(option) { - var outputExists = this.getInput('OUTPUTTYPE'); - var topExists = this.getInput('TOPTYPE'); - var bottomExists = this.getInput('BOTTOMTYPE'); - if (option === 'LEFT') { - if (!outputExists) { - this.addTypeInput_('OUTPUTTYPE', 'output type'); - } - } else if (outputExists) { - this.removeInput('OUTPUTTYPE'); - } - if (option === 'TOP' || option === 'BOTH') { - if (!topExists) { - this.addTypeInput_('TOPTYPE', 'top type'); - } - } else if (topExists) { - this.removeInput('TOPTYPE'); - } - if (option === 'BOTTOM' || option === 'BOTH') { - if (!bottomExists) { - this.addTypeInput_('BOTTOMTYPE', 'bottom type'); - } - } else if (bottomExists) { - this.removeInput('BOTTOMTYPE'); - } - }, - addTypeInput_: function(name, label) { - this.appendValueInput(name) - .setCheck('Type') - .appendField(label); - this.moveInputBefore(name, 'COLOUR'); - } -}; - -var FIELD_MESSAGE = 'fields %1 %2'; -var FIELD_ARGS = [ - { - "type": "field_dropdown", - "name": "ALIGN", - "options": [['left', 'LEFT'], ['right', 'RIGHT'], ['centre', 'CENTRE']], - }, - { - "type": "input_statement", - "name": "FIELDS", - "check": "Field" - } -]; - -var TYPE_MESSAGE = 'type %1'; -var TYPE_ARGS = [ - { - "type": "input_value", - "name": "TYPE", - "check": "Type", - "align": "RIGHT" - } -]; - -Blockly.Blocks['input_value'] = { - // Value input. - init: function() { - this.jsonInit({ - "message0": "value input %1 %2", - "args0": [ - { - "type": "field_input", - "name": "INPUTNAME", - "text": "NAME" - }, - { - "type": "input_dummy" - } - ], - "message1": FIELD_MESSAGE, - "args1": FIELD_ARGS, - "message2": TYPE_MESSAGE, - "args2": TYPE_ARGS, - "previousStatement": "Input", - "nextStatement": "Input", - "colour": 210, - "tooltip": "A value socket for horizontal connections.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=71" - }); - }, - onchange: function() { - inputNameCheck(this); - } -}; - -Blockly.Blocks['input_statement'] = { - // Statement input. - init: function() { - this.jsonInit({ - "message0": "statement input %1 %2", - "args0": [ - { - "type": "field_input", - "name": "INPUTNAME", - "text": "NAME" - }, - { - "type": "input_dummy" - }, - ], - "message1": FIELD_MESSAGE, - "args1": FIELD_ARGS, - "message2": TYPE_MESSAGE, - "args2": TYPE_ARGS, - "previousStatement": "Input", - "nextStatement": "Input", - "colour": 210, - "tooltip": "A statement socket for enclosed vertical stacks.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=246" - }); - }, - onchange: function() { - inputNameCheck(this); - } -}; - -Blockly.Blocks['input_dummy'] = { - // Dummy input. - init: function() { - this.jsonInit({ - "message0": "dummy input", - "message1": FIELD_MESSAGE, - "args1": FIELD_ARGS, - "previousStatement": "Input", - "nextStatement": "Input", - "colour": 210, - "tooltip": "For adding fields on a separate row with no " + - "connections. Alignment options (left, right, centre) " + - "apply only to multi-line fields.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=293" - }); - } -}; - -Blockly.Blocks['field_static'] = { - // Text value. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('text') - .appendField(new Blockly.FieldTextInput(''), 'TEXT'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Static text that serves as a label.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=88'); - } -}; - -Blockly.Blocks['field_input'] = { - // Text input. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('text input') - .appendField(new Blockly.FieldTextInput('default'), 'TEXT') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('An input field for the user to enter text.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=319'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_number'] = { - // Numeric input. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('numeric input') - .appendField(new Blockly.FieldNumber(0), 'VALUE') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.appendDummyInput() - .appendField('min') - .appendField(new Blockly.FieldNumber(-Infinity), 'MIN') - .appendField('max') - .appendField(new Blockly.FieldNumber(Infinity), 'MAX') - .appendField('precision') - .appendField(new Blockly.FieldNumber(0, 0), 'PRECISION'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('An input field for the user to enter a number.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=319'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_angle'] = { - // Angle input. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('angle input') - .appendField(new Blockly.FieldAngle('90'), 'ANGLE') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('An input field for the user to enter an angle.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=372'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_dropdown'] = { - // Dropdown menu. - init: function() { - this.appendDummyInput() - .appendField('dropdown') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.optionCount_ = 3; - this.updateShape_(); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setMutator(new Blockly.icons.MutatorIcon(['field_dropdown_option'])); - this.setColour(160); - this.setTooltip('Dropdown menu with a list of options.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386'); - }, - mutationToDom: function(workspace) { - // Create XML to represent menu options. - var container = Blockly.utils.xml.createElement('mutation'); - container.setAttribute('options', this.optionCount_); - return container; - }, - domToMutation: function(container) { - // Parse XML to restore the menu options. - this.optionCount_ = parseInt(container.getAttribute('options'), 10); - this.updateShape_(); - }, - decompose: function(workspace) { - // Populate the mutator's dialog with this block's components. - var containerBlock = workspace.newBlock('field_dropdown_container'); - containerBlock.initSvg(); - var connection = containerBlock.getInput('STACK').connection; - for (var i = 0; i < this.optionCount_; i++) { - var optionBlock = workspace.newBlock('field_dropdown_option'); - optionBlock.initSvg(); - connection.connect(optionBlock.previousConnection); - connection = optionBlock.nextConnection; - } - return containerBlock; - }, - compose: function(containerBlock) { - // Reconfigure this block based on the mutator dialog's components. - var optionBlock = containerBlock.getInputTargetBlock('STACK'); - // Count number of inputs. - var data = []; - while (optionBlock) { - data.push([optionBlock.userData_, optionBlock.cpuData_]); - optionBlock = optionBlock.nextConnection && - optionBlock.nextConnection.targetBlock(); - } - this.optionCount_ = data.length; - this.updateShape_(); - // Restore any data. - for (var i = 0; i < this.optionCount_; i++) { - this.setFieldValue(data[i][0] || 'option', 'USER' + i); - this.setFieldValue(data[i][1] || 'OPTIONNAME', 'CPU' + i); - } - }, - saveConnections: function(containerBlock) { - // Store names and values for each option. - var optionBlock = containerBlock.getInputTargetBlock('STACK'); - var i = 0; - while (optionBlock) { - optionBlock.userData_ = this.getFieldValue('USER' + i); - optionBlock.cpuData_ = this.getFieldValue('CPU' + i); - i++; - optionBlock = optionBlock.nextConnection && - optionBlock.nextConnection.targetBlock(); - } - }, - updateShape_: function() { - // Modify this block to have the correct number of options. - // Add new options. - for (var i = 0; i < this.optionCount_; i++) { - if (!this.getInput('OPTION' + i)) { - this.appendDummyInput('OPTION' + i) - .appendField(new Blockly.FieldTextInput('option'), 'USER' + i) - .appendField(',') - .appendField(new Blockly.FieldTextInput('OPTIONNAME'), 'CPU' + i); - } - } - // Remove deleted options. - while (this.getInput('OPTION' + i)) { - this.removeInput('OPTION' + i); - i++; - } - }, - onchange: function() { - if (this.workspace && this.optionCount_ < 1) { - this.setWarningText('Drop down menu must\nhave at least one option.'); - } else { - fieldNameCheck(this); - } - } -}; - -Blockly.Blocks['field_dropdown_container'] = { - // Container. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('add options'); - this.appendStatementInput('STACK'); - this.setTooltip('Add, remove, or reorder options\n' + - 'to reconfigure this dropdown menu.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386'); - this.contextMenu = false; - } -}; - -Blockly.Blocks['field_dropdown_option'] = { - // Add option. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('option'); - this.setPreviousStatement(true); - this.setNextStatement(true); - this.setTooltip('Add a new option to the dropdown menu.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386'); - this.contextMenu = false; - } -}; - -Blockly.Blocks['field_checkbox'] = { - // Checkbox. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('checkbox') - .appendField(new Blockly.FieldCheckbox('TRUE'), 'CHECKED') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Checkbox field.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=485'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_colour'] = { - // Colour input. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('colour') - .appendField(new Blockly.FieldColour('#ff0000'), 'COLOUR') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Colour input field.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=495'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_variable'] = { - // Dropdown for variables. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('variable') - .appendField(new Blockly.FieldTextInput('item'), 'TEXT') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Dropdown menu for variable names.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=510'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - -Blockly.Blocks['field_image'] = { - // Image. - init: function() { - this.setColour(160); - var src = 'https://www.gstatic.com/codesite/ph/images/star_on.gif'; - this.appendDummyInput() - .appendField('image') - .appendField(new Blockly.FieldTextInput(src), 'SRC'); - this.appendDummyInput() - .appendField('width') - .appendField(new Blockly.FieldNumber('15', 0, NaN, 1), 'WIDTH') - .appendField('height') - .appendField(new Blockly.FieldNumber('15', 0, NaN, 1), 'HEIGHT') - .appendField('alt text') - .appendField(new Blockly.FieldTextInput('*'), 'ALT'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Static image (JPEG, PNG, GIF, SVG, BMP).\n' + - 'Retains aspect ratio regardless of height and width.\n' + - 'Alt text is for when collapsed.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=567'); - } -}; - -Blockly.Blocks['type_group'] = { - // Group of types. - init: function() { - this.typeCount_ = 2; - this.updateShape_(); - this.setOutput(true, 'Type'); - this.setMutator(new Blockly.icons.MutatorIcon(['type_group_item'])); - this.setColour(230); - this.setTooltip('Allows more than one type to be accepted.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677'); - }, - mutationToDom: function(workspace) { - // Create XML to represent a group of types. - var container = Blockly.utils.xml.createElement('mutation'); - container.setAttribute('types', this.typeCount_); - return container; - }, - domToMutation: function(container) { - // Parse XML to restore the group of types. - this.typeCount_ = parseInt(container.getAttribute('types'), 10); - this.updateShape_(); - for (var i = 0; i < this.typeCount_; i++) { - this.removeInput('TYPE' + i); - } - for (var i = 0; i < this.typeCount_; i++) { - var input = this.appendValueInput('TYPE' + i) - .setCheck('Type'); - if (i === 0) { - input.appendField('any of'); - } - } - }, - decompose: function(workspace) { - // Populate the mutator's dialog with this block's components. - var containerBlock = workspace.newBlock('type_group_container'); - containerBlock.initSvg(); - var connection = containerBlock.getInput('STACK').connection; - for (var i = 0; i < this.typeCount_; i++) { - var typeBlock = workspace.newBlock('type_group_item'); - typeBlock.initSvg(); - connection.connect(typeBlock.previousConnection); - connection = typeBlock.nextConnection; - } - return containerBlock; - }, - compose: function(containerBlock) { - // Reconfigure this block based on the mutator dialog's components. - var typeBlock = containerBlock.getInputTargetBlock('STACK'); - // Count number of inputs. - var connections = []; - while (typeBlock) { - connections.push(typeBlock.valueConnection_); - typeBlock = typeBlock.nextConnection && - typeBlock.nextConnection.targetBlock(); - } - // Disconnect any children that don't belong. - for (var i = 0; i < this.typeCount_; i++) { - var connection = this.getInput('TYPE' + i).connection.targetConnection; - if (connection && !connections.includes(connection)) { - connection.disconnect(); - } - } - this.typeCount_ = connections.length; - this.updateShape_(); - // Reconnect any child blocks. - for (var i = 0; i < this.typeCount_; i++) { - connections[i]?.reconnect(this, 'TYPE' + i); - } - }, - saveConnections: function(containerBlock) { - // Store a pointer to any connected child blocks. - var typeBlock = containerBlock.getInputTargetBlock('STACK'); - var i = 0; - while (typeBlock) { - var input = this.getInput('TYPE' + i); - typeBlock.valueConnection_ = input && input.connection.targetConnection; - i++; - typeBlock = typeBlock.nextConnection && - typeBlock.nextConnection.targetBlock(); - } - }, - updateShape_: function() { - // Modify this block to have the correct number of inputs. - // Add new inputs. - for (var i = 0; i < this.typeCount_; i++) { - if (!this.getInput('TYPE' + i)) { - var input = this.appendValueInput('TYPE' + i); - if (i === 0) { - input.appendField('any of'); - } - } - } - // Remove deleted inputs. - while (this.getInput('TYPE' + i)) { - this.removeInput('TYPE' + i); - i++; - } - } -}; - -Blockly.Blocks['type_group_container'] = { - // Container. - init: function() { - this.jsonInit({ - "message0": "add types %1 %2", - "args0": [ - {"type": "input_dummy"}, - {"type": "input_statement", "name": "STACK"} - ], - "colour": 230, - "tooltip": "Add, or remove allowed type.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677" - }); - } -}; - -Blockly.Blocks['type_group_item'] = { - // Add type. - init: function() { - this.jsonInit({ - "message0": "type", - "previousStatement": null, - "nextStatement": null, - "colour": 230, - "tooltip": "Add a new allowed type.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677" - }); - } -}; - -Blockly.Blocks['type_null'] = { - // Null type. - valueType: null, - init: function() { - this.jsonInit({ - "message0": "any", - "output": "Type", - "colour": 230, - "tooltip": "Any type is allowed.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602" - }); - } -}; - -Blockly.Blocks['type_boolean'] = { - // Boolean type. - valueType: 'Boolean', - init: function() { - this.jsonInit({ - "message0": "Boolean", - "output": "Type", - "colour": 230, - "tooltip": "Booleans (true/false) are allowed.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602" - }); - } -}; - -Blockly.Blocks['type_number'] = { - // Number type. - valueType: 'Number', - init: function() { - this.jsonInit({ - "message0": "Number", - "output": "Type", - "colour": 230, - "tooltip": "Numbers (int/float) are allowed.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602" - }); - } -}; - -Blockly.Blocks['type_string'] = { - // String type. - valueType: 'String', - init: function() { - this.jsonInit({ - "message0": "String", - "output": "Type", - "colour": 230, - "tooltip": "Strings (text) are allowed.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602" - }); - } -}; - -Blockly.Blocks['type_list'] = { - // List type. - valueType: 'Array', - init: function() { - this.jsonInit({ - "message0": "Array", - "output": "Type", - "colour": 230, - "tooltip": "Arrays (lists) are allowed.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602" - }); - } -}; - -Blockly.Blocks['type_other'] = { - // Other type. - init: function() { - this.jsonInit({ - "message0": "other %1", - "args0": [{"type": "field_input", "name": "TYPE", "text": ""}], - "output": "Type", - "colour": 230, - "tooltip": "Custom type to allow.", - "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=702" - }); - } -}; - -Blockly.Blocks['colour_hue'] = { - // Set the colour of the block. - init: function() { - this.appendDummyInput() - .appendField('hue:') - .appendField(new Blockly.FieldAngle('0', this.validator), 'HUE'); - this.setOutput(true, 'Colour'); - this.setTooltip('Paint the block with this colour.'); - this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=55'); - }, - validator: function(text) { - // Update the current block's colour to match. - var hue = parseInt(text, 10); - if (!isNaN(hue)) { - this.sourceBlock_.setColour(hue); - } - }, - mutationToDom: function(workspace) { - var container = Blockly.utils.xml.createElement('mutation'); - container.setAttribute('colour', this.getColour()); - return container; - }, - domToMutation: function(container) { - this.setColour(container.getAttribute('colour')); - } -}; - -/** - * Check to see if more than one field has this name. - * Highly inefficient (On^2), but n is small. - * @param {!Blockly.Block} referenceBlock Block to check. - */ -function fieldNameCheck(referenceBlock) { - if (!referenceBlock.workspace) { - // Block has been deleted. - return; - } - var name = referenceBlock.getFieldValue('FIELDNAME').toLowerCase(); - var count = 0; - var blocks = referenceBlock.workspace.getAllBlocks(false); - for (var i = 0, block; block = blocks[i]; i++) { - var otherName = block.getFieldValue('FIELDNAME'); - if (!block.disabled && !block.getInheritedDisabled() && - otherName && otherName.toLowerCase() === name) { - count++; - } - } - var msg = (count > 1) ? - 'There are ' + count + ' field blocks\n with this name.' : null; - referenceBlock.setWarningText(msg); -} - -/** - * Check to see if more than one input has this name. - * Highly inefficient (On^2), but n is small. - * @param {!Blockly.Block} referenceBlock Block to check. - */ -function inputNameCheck(referenceBlock) { - if (!referenceBlock.workspace) { - // Block has been deleted. - return; - } - var name = referenceBlock.getFieldValue('INPUTNAME').toLowerCase(); - var count = 0; - var blocks = referenceBlock.workspace.getAllBlocks(false); - for (var i = 0, block; block = blocks[i]; i++) { - var otherName = block.getFieldValue('INPUTNAME'); - if (!block.disabled && !block.getInheritedDisabled() && - otherName && otherName.toLowerCase() === name) { - count++; - } - } - var msg = (count > 1) ? - 'There are ' + count + ' input blocks\n with this name.' : null; - referenceBlock.setWarningText(msg); -} diff --git a/demos/blockfactory_old/factory.js b/demos/blockfactory_old/factory.js deleted file mode 100644 index 2055b0609..000000000 --- a/demos/blockfactory_old/factory.js +++ /dev/null @@ -1,819 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview JavaScript for Blockly's Block Factory application. - */ -'use strict'; - -/** - * Workspace for user to build block. - * @type {Blockly.Workspace} - */ -var mainWorkspace = null; - -/** - * Workspace for preview of block. - * @type {Blockly.Workspace} - */ -var previewWorkspace = null; - -/** - * Name of block if not named. - */ -var UNNAMED = 'unnamed'; - -/** - * Change the language code format. - */ -function formatChange() { - var mask = document.getElementById('blocklyMask'); - var languagePre = document.getElementById('languagePre'); - var languageTA = document.getElementById('languageTA'); - if (document.getElementById('format').value === 'Manual') { - Blockly.common.getMainWorkspace().hideChaff(); - mask.style.display = 'block'; - languagePre.style.display = 'none'; - languageTA.style.display = 'block'; - var code = languagePre.textContent.trim(); - languageTA.value = code; - languageTA.focus(); - updatePreview(); - } else { - mask.style.display = 'none'; - languageTA.style.display = 'none'; - languagePre.style.display = 'block'; - updateLanguage(); - } - disableEnableLink(); -} - -/** - * Update the language code based on constructs made in Blockly. - */ -function updateLanguage() { - var rootBlock = getRootBlock(); - if (!rootBlock) { - return; - } - var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase(); - if (!blockType) { - blockType = UNNAMED; - } - blockType = blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1'); - switch (document.getElementById('format').value) { - case 'JSON': - var code = formatJson_(blockType, rootBlock); - break; - case 'JavaScript': - var code = formatJavaScript_(blockType, rootBlock); - break; - } - injectCode(code, 'languagePre'); - updatePreview(); -} - -/** - * Update the language code as JSON. - * @param {string} blockType Name of block. - * @param {!Blockly.Block} rootBlock Factory_base block. - * @return {string} Generated language code. - * @private - */ -function formatJson_(blockType, rootBlock) { - var JS = {}; - // Type is not used by Blockly, but may be used by a loader. - JS.type = blockType; - // Generate inputs. - var message = []; - var args = []; - var contentsBlock = rootBlock.getInputTargetBlock('INPUTS'); - var lastInput = null; - while (contentsBlock) { - if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { - var fields = getFieldsJson_(contentsBlock.getInputTargetBlock('FIELDS')); - for (var i = 0; i < fields.length; i++) { - if (typeof fields[i] === 'string') { - message.push(fields[i].replace(/%/g, '%%')); - } else { - args.push(fields[i]); - message.push('%' + args.length); - } - } - - var input = {type: contentsBlock.type}; - // Dummy inputs don't have names. Other inputs do. - if (contentsBlock.type !== 'input_dummy') { - input.name = contentsBlock.getFieldValue('INPUTNAME'); - } - var check = JSON.parse(getOptTypesFrom(contentsBlock, 'TYPE') || 'null'); - if (check) { - input.check = check; - } - var align = contentsBlock.getFieldValue('ALIGN'); - if (align !== 'LEFT') { - input.align = align; - } - args.push(input); - message.push('%' + args.length); - lastInput = contentsBlock; - } - contentsBlock = contentsBlock.nextConnection && - contentsBlock.nextConnection.targetBlock(); - } - // Remove last input if dummy and not empty. - if (lastInput && lastInput.type === 'input_dummy') { - var fields = lastInput.getInputTargetBlock('FIELDS'); - if (fields && getFieldsJson_(fields).join('').trim() !== '') { - var align = lastInput.getFieldValue('ALIGN'); - if (align !== 'LEFT') { - JS.lastDummyAlign0 = align; - } - args.pop(); - message.pop(); - } - } - JS.message0 = message.join(' '); - if (args.length) { - JS.args0 = args; - } - // Generate inline/external switch. - if (rootBlock.getFieldValue('INLINE') === 'EXT') { - JS.inputsInline = false; - } else if (rootBlock.getFieldValue('INLINE') === 'INT') { - JS.inputsInline = true; - } - // Generate output, or next/previous connections. - switch (rootBlock.getFieldValue('CONNECTIONS')) { - case 'LEFT': - JS.output = - JSON.parse(getOptTypesFrom(rootBlock, 'OUTPUTTYPE') || 'null'); - break; - case 'BOTH': - JS.previousStatement = - JSON.parse(getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null'); - JS.nextStatement = - JSON.parse(getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null'); - break; - case 'TOP': - JS.previousStatement = - JSON.parse(getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null'); - break; - case 'BOTTOM': - JS.nextStatement = - JSON.parse(getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null'); - break; - } - // Generate colour. - var colourBlock = rootBlock.getInputTargetBlock('COLOUR'); - if (colourBlock && !colourBlock.disabled) { - var hue = parseInt(colourBlock.getFieldValue('HUE'), 10); - JS.colour = hue; - } - JS.tooltip = ''; - JS.helpUrl = 'http://www.example.com/'; - return JSON.stringify(JS, null, ' '); -} - -/** - * Update the language code as JavaScript. - * @param {string} blockType Name of block. - * @param {!Blockly.Block} rootBlock Factory_base block. - * @return {string} Generated language code. - * @private - */ -function formatJavaScript_(blockType, rootBlock) { - var code = []; - code.push("Blockly.Blocks['" + blockType + "'] = {"); - code.push(" init: function() {"); - // Generate inputs. - var TYPES = {'input_value': 'appendValueInput', - 'input_statement': 'appendStatementInput', - 'input_dummy': 'appendDummyInput'}; - var contentsBlock = rootBlock.getInputTargetBlock('INPUTS'); - while (contentsBlock) { - if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { - var name = ''; - // Dummy inputs don't have names. Other inputs do. - if (contentsBlock.type !== 'input_dummy') { - name = escapeString(contentsBlock.getFieldValue('INPUTNAME')); - } - code.push(' this.' + TYPES[contentsBlock.type] + '(' + name + ')'); - var check = getOptTypesFrom(contentsBlock, 'TYPE'); - if (check) { - code.push(' .setCheck(' + check + ')'); - } - var align = contentsBlock.getFieldValue('ALIGN'); - if (align !== 'LEFT') { - code.push(' .setAlign(Blockly.ALIGN_' + align + ')'); - } - var fields = getFieldsJs_(contentsBlock.getInputTargetBlock('FIELDS')); - for (var i = 0; i < fields.length; i++) { - code.push(' .appendField(' + fields[i] + ')'); - } - // Add semicolon to last line to finish the statement. - code[code.length - 1] += ';'; - } - contentsBlock = contentsBlock.nextConnection && - contentsBlock.nextConnection.targetBlock(); - } - // Generate inline/external switch. - if (rootBlock.getFieldValue('INLINE') === 'EXT') { - code.push(' this.setInputsInline(false);'); - } else if (rootBlock.getFieldValue('INLINE') === 'INT') { - code.push(' this.setInputsInline(true);'); - } - // Generate output, or next/previous connections. - switch (rootBlock.getFieldValue('CONNECTIONS')) { - case 'LEFT': - code.push(connectionLineJs_('setOutput', 'OUTPUTTYPE')); - break; - case 'BOTH': - code.push(connectionLineJs_('setPreviousStatement', 'TOPTYPE')); - code.push(connectionLineJs_('setNextStatement', 'BOTTOMTYPE')); - break; - case 'TOP': - code.push(connectionLineJs_('setPreviousStatement', 'TOPTYPE')); - break; - case 'BOTTOM': - code.push(connectionLineJs_('setNextStatement', 'BOTTOMTYPE')); - break; - } - // Generate colour. - var colourBlock = rootBlock.getInputTargetBlock('COLOUR'); - if (colourBlock && !colourBlock.disabled) { - var hue = parseInt(colourBlock.getFieldValue('HUE'), 10); - if (!isNaN(hue)) { - code.push(' this.setColour(' + hue + ');'); - } - } - code.push(" this.setTooltip('');"); - code.push(" this.setHelpUrl('http://www.example.com/');"); - code.push(' }'); - code.push('};'); - return code.join('\n'); -} - -/** - * Create JS code required to create a top, bottom, or value connection. - * @param {string} functionName JavaScript function name. - * @param {string} typeName Name of type input. - * @return {string} Line of JavaScript code to create connection. - * @private - */ -function connectionLineJs_(functionName, typeName) { - var type = getOptTypesFrom(getRootBlock(), typeName); - if (type) { - type = ', ' + type; - } else { - type = ''; - } - return ' this.' + functionName + '(true' + type + ');'; -} - -/** - * Returns field strings and any config. - * @param {!Blockly.Block} block Input block. - * @return {!Array} Field strings. - * @private - */ -function getFieldsJs_(block) { - var fields = []; - while (block) { - if (!block.disabled && !block.getInheritedDisabled()) { - switch (block.type) { - case 'field_static': - // Result: 'hello' - fields.push(escapeString(block.getFieldValue('TEXT'))); - break; - case 'field_input': - // Result: new Blockly.FieldTextInput('Hello'), 'GREET' - fields.push('new Blockly.FieldTextInput(' + - escapeString(block.getFieldValue('TEXT')) + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_number': - // Result: new Blockly.FieldNumber(10, 0, 100, 1), 'NUMBER' - var args = [ - Number(block.getFieldValue('VALUE')), - Number(block.getFieldValue('MIN')), - Number(block.getFieldValue('MAX')), - Number(block.getFieldValue('PRECISION')) - ]; - // Remove any trailing arguments that aren't needed. - if (args[3] === 0) { - args.pop(); - if (args[2] === Infinity) { - args.pop(); - if (args[1] === -Infinity) { - args.pop(); - } - } - } - fields.push('new Blockly.FieldNumber(' + args.join(', ') + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_angle': - // Result: new Blockly.FieldAngle(90), 'ANGLE' - fields.push('new Blockly.FieldAngle(' + - Number(block.getFieldValue('ANGLE')) + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_checkbox': - // Result: new Blockly.FieldCheckbox('TRUE'), 'CHECK' - fields.push('new Blockly.FieldCheckbox(' + - escapeString(block.getFieldValue('CHECKED')) + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_colour': - // Result: new Blockly.FieldColour('#ff0000'), 'COLOUR' - fields.push('new Blockly.FieldColour(' + - escapeString(block.getFieldValue('COLOUR')) + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_variable': - // Result: new Blockly.FieldVariable('item'), 'VAR' - var varname = escapeString(block.getFieldValue('TEXT') || null); - fields.push('new Blockly.FieldVariable(' + varname + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; - case 'field_dropdown': - // Result: - // new Blockly.FieldDropdown([['yes', '1'], ['no', '0']]), 'TOGGLE' - var options = []; - for (var i = 0; i < block.optionCount_; i++) { - options[i] = '[' + escapeString(block.getFieldValue('USER' + i)) + - ', ' + escapeString(block.getFieldValue('CPU' + i)) + ']'; - } - if (options.length) { - fields.push('new Blockly.FieldDropdown([' + - options.join(', ') + ']), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - } - break; - case 'field_image': - // Result: new Blockly.FieldImage('http://...', 80, 60, '*') - var src = escapeString(block.getFieldValue('SRC')); - var width = Number(block.getFieldValue('WIDTH')); - var height = Number(block.getFieldValue('HEIGHT')); - var alt = escapeString(block.getFieldValue('ALT')); - fields.push('new Blockly.FieldImage(' + - src + ', ' + width + ', ' + height + ', ' + alt + ')'); - break; - } - } - block = block.nextConnection && block.nextConnection.targetBlock(); - } - return fields; -} - -/** - * Returns field strings and any config. - * @param {!Blockly.Block} block Input block. - * @return {!Array} Array of static text and field configs. - * @private - */ -function getFieldsJson_(block) { - var fields = []; - while (block) { - if (!block.disabled && !block.getInheritedDisabled()) { - switch (block.type) { - case 'field_static': - // Result: 'hello' - fields.push(block.getFieldValue('TEXT')); - break; - case 'field_input': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - text: block.getFieldValue('TEXT') - }); - break; - case 'field_number': - var obj = { - type: block.type, - name: block.getFieldValue('FIELDNAME'), - value: Number(block.getFieldValue('VALUE')) - }; - var min = Number(block.getFieldValue('MIN')); - if (min > -Infinity) { - obj.min = min; - } - var max = Number(block.getFieldValue('MAX')); - if (max < Infinity) { - obj.max = max; - } - var precision = Number(block.getFieldValue('PRECISION')); - if (precision) { - obj.precision = precision; - } - fields.push(obj); - break; - case 'field_angle': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - angle: Number(block.getFieldValue('ANGLE')) - }); - break; - case 'field_checkbox': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - checked: block.getFieldValue('CHECKED') === 'TRUE' - }); - break; - case 'field_colour': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - colour: block.getFieldValue('COLOUR') - }); - break; - case 'field_variable': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - variable: block.getFieldValue('TEXT') || null - }); - break; - case 'field_dropdown': - var options = []; - for (var i = 0; i < block.optionCount_; i++) { - options[i] = [block.getFieldValue('USER' + i), - block.getFieldValue('CPU' + i)]; - } - if (options.length) { - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - options: options - }); - } - break; - case 'field_image': - fields.push({ - type: block.type, - src: block.getFieldValue('SRC'), - width: Number(block.getFieldValue('WIDTH')), - height: Number(block.getFieldValue('HEIGHT')), - alt: block.getFieldValue('ALT') - }); - break; - } - } - block = block.nextConnection && block.nextConnection.targetBlock(); - } - return fields; -} - -/** - * Escape a string. - * @param {string} string String to escape. - * @return {string} Escaped string surrounded by quotes. - */ -function escapeString(string) { - return JSON.stringify(string); -} - -/** - * Fetch the type(s) defined in the given input. - * Format as a string for appending to the generated code. - * @param {!Blockly.Block} block Block with input. - * @param {string} name Name of the input. - * @return {?string} String defining the types. - */ -function getOptTypesFrom(block, name) { - var types = getTypesFrom_(block, name); - if (types.length === 0) { - return undefined; - } else if (types.includes('null')) { - return 'null'; - } else if (types.length === 1) { - return types[0]; - } else { - return '[' + types.join(', ') + ']'; - } -} - -/** - * Fetch the type(s) defined in the given input. - * @param {!Blockly.Block} block Block with input. - * @param {string} name Name of the input. - * @return {!Array} List of types. - * @private - */ -function getTypesFrom_(block, name) { - var typeBlock = block.getInputTargetBlock(name); - var types; - if (!typeBlock || typeBlock.disabled) { - types = []; - } else if (typeBlock.type === 'type_other') { - types = [escapeString(typeBlock.getFieldValue('TYPE'))]; - } else if (typeBlock.type === 'type_group') { - types = []; - for (var i = 0; i < typeBlock.typeCount_; i++) { - types = types.concat(getTypesFrom_(typeBlock, 'TYPE' + i)); - } - // Remove duplicates. - var hash = Object.create(null); - for (var n = types.length - 1; n >= 0; n--) { - if (hash[types[n]]) { - types.splice(n, 1); - } - hash[types[n]] = true; - } - } else { - types = [escapeString(typeBlock.valueType)]; - } - return types; -} - -/** - * Update the generator code. - * @param {!Blockly.Block} block Rendered block in preview workspace. - */ -function updateGenerator(block) { - function makeVar(root, name) { - name = name.toLowerCase().replace(/\W/g, '_'); - return ' var ' + root + '_' + name; - } - var language = document.getElementById('language').value; - var code = []; - code.push("Blockly." + language + "['" + block.type + - "'] = function(block) {"); - - // Generate getters for any fields or inputs. - for (var i = 0, input; input = block.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { - var name = field.name; - if (!name) { - continue; - } - if (field instanceof Blockly.FieldVariable) { - // Subclass of Blockly.FieldDropdown, must test first. - code.push(makeVar('variable', name) + - " = Blockly." + language + - ".nameDB_.getName(block.getFieldValue('" + name + - "'), Blockly.Variables.NAME_TYPE);"); - } else if (field instanceof Blockly.FieldAngle) { - // Subclass of Blockly.FieldTextInput, must test first. - code.push(makeVar('angle', name) + - " = block.getFieldValue('" + name + "');"); - } else if (field instanceof Blockly.FieldColour) { - code.push(makeVar('colour', name) + - " = block.getFieldValue('" + name + "');"); - } else if (field instanceof Blockly.FieldCheckbox) { - code.push(makeVar('checkbox', name) + - " = block.getFieldValue('" + name + "') === 'TRUE';"); - } else if (field instanceof Blockly.FieldDropdown) { - code.push(makeVar('dropdown', name) + - " = block.getFieldValue('" + name + "');"); - } else if (field instanceof Blockly.FieldNumber) { - code.push(makeVar('number', name) + - " = block.getFieldValue('" + name + "');"); - } else if (field instanceof Blockly.FieldTextInput) { - code.push(makeVar('text', name) + - " = block.getFieldValue('" + name + "');"); - } - } - var name = input.name; - if (name) { - if (input.type === Blockly.INPUT_VALUE) { - code.push(makeVar('value', name) + - " = Blockly." + language + ".valueToCode(block, '" + name + - "', Blockly." + language + ".ORDER_ATOMIC);"); - } else if (input.type === Blockly.NEXT_STATEMENT) { - code.push(makeVar('statements', name) + - " = Blockly." + language + ".statementToCode(block, '" + - name + "');"); - } - } - } - // Most languages end lines with a semicolon. Python does not. - var lineEnd = { - 'JavaScript': ';', - 'Python': '', - 'PHP': ';', - 'Dart': ';' - }; - code.push(" // TODO: Assemble " + language + " into code variable."); - if (block.outputConnection) { - code.push(" var code = '...';"); - code.push(" // TODO: Change ORDER_NONE to the correct strength."); - code.push(" return [code, Blockly." + language + ".ORDER_NONE];"); - } else { - code.push(" var code = '..." + (lineEnd[language] || '') + "\\n';"); - code.push(" return code;"); - } - code.push("};"); - - injectCode(code.join('\n'), 'generatorPre'); -} - -/** - * Existing direction ('ltr' vs 'rtl') of preview. - */ -var oldDir = null; - -/** - * Update the preview display. - */ -function updatePreview() { - // Toggle between LTR/RTL if needed (also used in first display). - var newDir = document.getElementById('direction').value; - if (oldDir !== newDir) { - if (previewWorkspace) { - previewWorkspace.dispose(); - } - var rtl = newDir === 'rtl'; - previewWorkspace = Blockly.inject('preview', - {rtl: rtl, - media: '../../media/', - scrollbars: true}); - oldDir = newDir; - } - previewWorkspace.clear(); - - // Fetch the code and determine its format (JSON or JavaScript). - var format = document.getElementById('format').value; - if (format === 'Manual') { - var code = document.getElementById('languageTA').value; - // If the code is JSON, it will parse, otherwise treat as JS. - try { - JSON.parse(code); - format = 'JSON'; - } catch (e) { - format = 'JavaScript'; - } - } else { - var code = document.getElementById('languagePre').textContent; - } - if (!code.trim()) { - // Nothing to render. Happens while cloud storage is loading. - return; - } - - // Backup Blockly.Blocks object so that main workspace and preview don't - // collide if user creates a 'factory_base' block, for instance. - var backupBlocks = Blockly.Blocks; - try { - // Make a shallow copy. - Blockly.Blocks = {}; - for (var prop in backupBlocks) { - Blockly.Blocks[prop] = backupBlocks[prop]; - } - - if (format === 'JSON') { - var json = JSON.parse(code); - Blockly.Blocks[json.type || UNNAMED] = { - init: function() { - this.jsonInit(json); - } - }; - } else if (format === 'JavaScript') { - eval(code); - } else { - throw 'Unknown format: ' + format; - } - - // Look for a block on Blockly.Blocks that does not match the backup. - var blockType = null; - for (var type in Blockly.Blocks) { - if (typeof Blockly.Blocks[type].init === 'function' && - Blockly.Blocks[type] !== backupBlocks[type]) { - blockType = type; - break; - } - } - if (!blockType) { - return; - } - - // Create the preview block. - var previewBlock = previewWorkspace.newBlock(blockType); - previewBlock.initSvg(); - previewBlock.render(); - previewBlock.setMovable(false); - previewBlock.setDeletable(false); - previewBlock.moveBy(15, 10); - previewWorkspace.clearUndo(); - - updateGenerator(previewBlock); - } finally { - Blockly.Blocks = backupBlocks; - } -} - -/** - * Inject code into a pre tag, with syntax highlighting. - * Safe from HTML/script injection. - * @param {string} code Lines of code. - * @param {string} id ID of
 element to inject into.
- */
-function injectCode(code, id) {
-  var pre = document.getElementById(id);
-  pre.textContent = code;
-  // Remove the 'prettyprinted' class, so that Prettify will recalculate.
-  pre.className = pre.className.replace('prettyprinted', '');
-  PR.prettyPrint();
-}
-
-/**
- * Return the uneditable container block that everything else attaches to.
- * @return {Blockly.Block}
- */
-function getRootBlock() {
-  var blocks = mainWorkspace.getTopBlocks(false);
-  for (var i = 0, block; block = blocks[i]; i++) {
-    if (block.type === 'factory_base') {
-      return block;
-    }
-  }
-  return null;
-}
-
-/**
- * Disable the link button if the format is 'Manual', enable otherwise.
- */
-function disableEnableLink() {
-  var linkButton = document.getElementById('linkButton');
-  linkButton.disabled = document.getElementById('format').value === 'Manual';
-}
-
-/**
- * Initialize Blockly and layout.  Called on page load.
- */
-function init() {
-  if ('BlocklyStorage' in window) {
-    BlocklyStorage.HTTPREQUEST_ERROR =
-        'There was a problem with the request.\n';
-    BlocklyStorage.LINK_ALERT =
-        'Share your blocks with this link:\n\n%1';
-    BlocklyStorage.HASH_ERROR =
-        'Sorry, "%1" doesn\'t correspond with any saved Blockly file.';
-    BlocklyStorage.XML_ERROR = 'Could not load your saved file.\n'+
-        'Perhaps it was created with a different version of Blockly?';
-    var linkButton = document.getElementById('linkButton');
-    linkButton.style.display = 'inline-block';
-    linkButton.addEventListener('click',
-        function() {BlocklyStorage.link(mainWorkspace);});
-    disableEnableLink();
-  }
-
-  document.getElementById('helpButton').addEventListener('click',
-    function() {
-      open('https://developers.google.com/blockly/guides/create-custom-blocks/block-factory',
-           'BlockFactoryHelp');
-    });
-
-  var expandList = [
-    document.getElementById('blockly'),
-    document.getElementById('blocklyMask'),
-    document.getElementById('preview'),
-    document.getElementById('languagePre'),
-    document.getElementById('languageTA'),
-    document.getElementById('generatorPre')
-  ];
-  var onresize = function(e) {
-    for (var i = 0, expand; expand = expandList[i]; i++) {
-      expand.style.width = (expand.parentNode.offsetWidth - 2) + 'px';
-      expand.style.height = (expand.parentNode.offsetHeight - 2) + 'px';
-    }
-  };
-  onresize();
-  window.addEventListener('resize', onresize);
-
-  var toolbox = document.getElementById('toolbox');
-  mainWorkspace = Blockly.inject('blockly',
-      {collapse: false,
-       toolbox: toolbox,
-       media: '../../media/'});
-
-  // Create the root block.
-  if ('BlocklyStorage' in window && window.location.hash.length > 1) {
-    BlocklyStorage.retrieveXml(window.location.hash.substring(1),
-                               mainWorkspace);
-  } else {
-    var xml = '';
-    Blockly.Xml.domToWorkspace(Blockly.utils.xml.textToDom(xml), mainWorkspace);
-  }
-  mainWorkspace.clearUndo();
-
-  mainWorkspace.addChangeListener(Blockly.Events.disableOrphans);
-  mainWorkspace.addChangeListener(updateLanguage);
-  document.getElementById('direction')
-      .addEventListener('change', updatePreview);
-  document.getElementById('languageTA')
-      .addEventListener('change', updatePreview);
-  document.getElementById('languageTA')
-      .addEventListener('keyup', updatePreview);
-  document.getElementById('format')
-      .addEventListener('change', formatChange);
-  document.getElementById('language')
-      .addEventListener('change', updatePreview);
-}
-window.addEventListener('load', init);
diff --git a/demos/blockfactory_old/icon.png b/demos/blockfactory_old/icon.png
deleted file mode 100644
index 4f8b72f41..000000000
Binary files a/demos/blockfactory_old/icon.png and /dev/null differ
diff --git a/demos/blockfactory_old/index.html b/demos/blockfactory_old/index.html
deleted file mode 100644
index 2509e7138..000000000
--- a/demos/blockfactory_old/index.html
+++ /dev/null
@@ -1,224 +0,0 @@
-
-
-
-  
-  
-  Blockly Demo: Block Factory
-  
-  
-  
-  
-  
-  
-  
-
-
-  
-    
-      
-      
-    
-    
-      
-      
-    
-  
-

Blockly > - Demos > Block Factory

-
- - - - - -
-

Preview: - -

-
- - - -
-
-
-
-
- - - - - - - - - - - - - - - - -
-
-
-

Language code: - -

-
-

-              
-            
-

Generator stub: - -

-
-

-            
-
- - - diff --git a/demos/blockfactory_old/link.png b/demos/blockfactory_old/link.png deleted file mode 100644 index 11dfd8284..000000000 Binary files a/demos/blockfactory_old/link.png and /dev/null differ diff --git a/generators/dart.ts b/generators/dart.ts index ee21b1fbd..5587e94f1 100644 --- a/generators/dart.ts +++ b/generators/dart.ts @@ -13,7 +13,6 @@ // Former goog.module ID: Blockly.Dart.all import {DartGenerator} from './dart/dart_generator.js'; -import * as colour from './dart/colour.js'; import * as lists from './dart/lists.js'; import * as logic from './dart/logic.js'; import * as loops from './dart/loops.js'; @@ -37,7 +36,6 @@ dartGenerator.addReservedWords('Html,Math'); // Install per-block-type generator functions: const generators: typeof dartGenerator.forBlock = { - ...colour, ...lists, ...logic, ...loops, diff --git a/generators/dart/colour.ts b/generators/dart/colour.ts deleted file mode 100644 index ac72fc04c..000000000 --- a/generators/dart/colour.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * @license - * Copyright 2014 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @file Generating Dart for colour blocks. - */ - -// Former goog.module ID: Blockly.Dart.colour - -import type {Block} from '../../core/block.js'; -import type {DartGenerator} from './dart_generator.js'; -import {Order} from './dart_generator.js'; - -// RESERVED WORDS: 'Math' - -export function colour_picker( - block: Block, - generator: DartGenerator, -): [string, Order] { - // Colour picker. - const code = generator.quote_(block.getFieldValue('COLOUR')); - return [code, Order.ATOMIC]; -} - -export function colour_random( - block: Block, - generator: DartGenerator, -): [string, Order] { - // Generate a random colour. - // TODO(#7600): find better approach than casting to any to override - // CodeGenerator declaring .definitions protected. - (generator as AnyDuringMigration).definitions_['import_dart_math'] = - "import 'dart:math' as Math;"; - const functionName = generator.provideFunction_( - 'colour_random', - ` -String ${generator.FUNCTION_NAME_PLACEHOLDER_}() { - String hex = '0123456789abcdef'; - var rnd = new Math.Random(); - return '#\${hex[rnd.nextInt(16)]}\${hex[rnd.nextInt(16)]}' - '\${hex[rnd.nextInt(16)]}\${hex[rnd.nextInt(16)]}' - '\${hex[rnd.nextInt(16)]}\${hex[rnd.nextInt(16)]}'; -} -`, - ); - const code = functionName + '()'; - return [code, Order.UNARY_POSTFIX]; -} - -export function colour_rgb( - block: Block, - generator: DartGenerator, -): [string, Order] { - // Compose a colour from RGB components expressed as percentages. - const red = generator.valueToCode(block, 'RED', Order.NONE) || 0; - const green = generator.valueToCode(block, 'GREEN', Order.NONE) || 0; - const blue = generator.valueToCode(block, 'BLUE', Order.NONE) || 0; - - // TODO(#7600): find better approach than casting to any to override - // CodeGenerator declaring .definitions protected. - (generator as AnyDuringMigration).definitions_['import_dart_math'] = - "import 'dart:math' as Math;"; - const functionName = generator.provideFunction_( - 'colour_rgb', - ` -String ${generator.FUNCTION_NAME_PLACEHOLDER_}(num r, num g, num b) { - num rn = (Math.max(Math.min(r, 100), 0) * 2.55).round(); - String rs = rn.toInt().toRadixString(16); - rs = '0$rs'; - rs = rs.substring(rs.length - 2); - num gn = (Math.max(Math.min(g, 100), 0) * 2.55).round(); - String gs = gn.toInt().toRadixString(16); - gs = '0$gs'; - gs = gs.substring(gs.length - 2); - num bn = (Math.max(Math.min(b, 100), 0) * 2.55).round(); - String bs = bn.toInt().toRadixString(16); - bs = '0$bs'; - bs = bs.substring(bs.length - 2); - return '#$rs$gs$bs'; -} -`, - ); - const code = functionName + '(' + red + ', ' + green + ', ' + blue + ')'; - return [code, Order.UNARY_POSTFIX]; -} - -export function colour_blend( - block: Block, - generator: DartGenerator, -): [string, Order] { - // Blend two colours together. - const c1 = generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; - const c2 = generator.valueToCode(block, 'COLOUR2', Order.NONE) || "'#000000'"; - const ratio = generator.valueToCode(block, 'RATIO', Order.NONE) || 0.5; - - // TODO(#7600): find better approach than casting to any to override - // CodeGenerator declaring .definitions protected. - (generator as AnyDuringMigration).definitions_['import_dart_math'] = - "import 'dart:math' as Math;"; - const functionName = generator.provideFunction_( - 'colour_blend', - ` -String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String c1, String c2, num ratio) { - ratio = Math.max(Math.min(ratio, 1), 0); - int r1 = int.parse('0x\${c1.substring(1, 3)}'); - int g1 = int.parse('0x\${c1.substring(3, 5)}'); - int b1 = int.parse('0x\${c1.substring(5, 7)}'); - int r2 = int.parse('0x\${c2.substring(1, 3)}'); - int g2 = int.parse('0x\${c2.substring(3, 5)}'); - int b2 = int.parse('0x\${c2.substring(5, 7)}'); - num rn = (r1 * (1 - ratio) + r2 * ratio).round(); - String rs = rn.toInt().toRadixString(16); - num gn = (g1 * (1 - ratio) + g2 * ratio).round(); - String gs = gn.toInt().toRadixString(16); - num bn = (b1 * (1 - ratio) + b2 * ratio).round(); - String bs = bn.toInt().toRadixString(16); - rs = '0$rs'; - rs = rs.substring(rs.length - 2); - gs = '0$gs'; - gs = gs.substring(gs.length - 2); - bs = '0$bs'; - bs = bs.substring(bs.length - 2); - return '#$rs$gs$bs'; -} -`, - ); - const code = functionName + '(' + c1 + ', ' + c2 + ', ' + ratio + ')'; - return [code, Order.UNARY_POSTFIX]; -} diff --git a/generators/dart/text.ts b/generators/dart/text.ts index 6bb2bea82..c141eaad0 100644 --- a/generators/dart/text.ts +++ b/generators/dart/text.ts @@ -23,16 +23,6 @@ export function text(block: Block, generator: DartGenerator): [string, Order] { return [code, Order.ATOMIC]; } -export function text_multiline( - block: Block, - generator: DartGenerator, -): [string, Order] { - // Text value. - const code = generator.multiline_quote_(block.getFieldValue('TEXT')); - const order = code.includes('+') ? Order.ADDITIVE : Order.ATOMIC; - return [code, order]; -} - export function text_join( block: Block, generator: DartGenerator, diff --git a/generators/javascript.ts b/generators/javascript.ts index 2358d187d..5fa6bb84d 100644 --- a/generators/javascript.ts +++ b/generators/javascript.ts @@ -13,7 +13,6 @@ // Former goog.module ID: Blockly.JavaScript.all import {JavascriptGenerator} from './javascript/javascript_generator.js'; -import * as colour from './javascript/colour.js'; import * as lists from './javascript/lists.js'; import * as logic from './javascript/logic.js'; import * as loops from './javascript/loops.js'; @@ -33,7 +32,6 @@ export const javascriptGenerator = new JavascriptGenerator(); // Install per-block-type generator functions: const generators: typeof javascriptGenerator.forBlock = { - ...colour, ...lists, ...logic, ...loops, diff --git a/generators/javascript/colour.ts b/generators/javascript/colour.ts deleted file mode 100644 index b599e76d9..000000000 --- a/generators/javascript/colour.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @file Generating JavaScript for colour blocks. - */ - -// Former goog.module ID: Blockly.JavaScript.colour - -import type {Block} from '../../core/block.js'; -import type {JavascriptGenerator} from './javascript_generator.js'; -import {Order} from './javascript_generator.js'; - -export function colour_picker( - block: Block, - generator: JavascriptGenerator, -): [string, Order] { - // Colour picker. - const code = generator.quote_(block.getFieldValue('COLOUR')); - return [code, Order.ATOMIC]; -} - -export function colour_random( - block: Block, - generator: JavascriptGenerator, -): [string, Order] { - // Generate a random colour. - const functionName = generator.provideFunction_( - 'colourRandom', - ` -function ${generator.FUNCTION_NAME_PLACEHOLDER_}() { - var num = Math.floor(Math.random() * Math.pow(2, 24)); - return '#' + ('00000' + num.toString(16)).substr(-6); -} -`, - ); - const code = functionName + '()'; - return [code, Order.FUNCTION_CALL]; -} - -export function colour_rgb( - block: Block, - generator: JavascriptGenerator, -): [string, Order] { - // Compose a colour from RGB components expressed as percentages. - const red = generator.valueToCode(block, 'RED', Order.NONE) || 0; - const green = generator.valueToCode(block, 'GREEN', Order.NONE) || 0; - const blue = generator.valueToCode(block, 'BLUE', Order.NONE) || 0; - const functionName = generator.provideFunction_( - 'colourRgb', - ` -function ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b) { - r = Math.max(Math.min(Number(r), 100), 0) * 2.55; - g = Math.max(Math.min(Number(g), 100), 0) * 2.55; - b = Math.max(Math.min(Number(b), 100), 0) * 2.55; - r = ('0' + (Math.round(r) || 0).toString(16)).slice(-2); - g = ('0' + (Math.round(g) || 0).toString(16)).slice(-2); - b = ('0' + (Math.round(b) || 0).toString(16)).slice(-2); - return '#' + r + g + b; -} -`, - ); - const code = functionName + '(' + red + ', ' + green + ', ' + blue + ')'; - return [code, Order.FUNCTION_CALL]; -} - -export function colour_blend( - block: Block, - generator: JavascriptGenerator, -): [string, Order] { - // Blend two colours together. - const c1 = generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; - const c2 = generator.valueToCode(block, 'COLOUR2', Order.NONE) || "'#000000'"; - const ratio = generator.valueToCode(block, 'RATIO', Order.NONE) || 0.5; - const functionName = generator.provideFunction_( - 'colourBlend', - ` -function ${generator.FUNCTION_NAME_PLACEHOLDER_}(c1, c2, ratio) { - ratio = Math.max(Math.min(Number(ratio), 1), 0); - var r1 = parseInt(c1.substring(1, 3), 16); - var g1 = parseInt(c1.substring(3, 5), 16); - var b1 = parseInt(c1.substring(5, 7), 16); - var r2 = parseInt(c2.substring(1, 3), 16); - var g2 = parseInt(c2.substring(3, 5), 16); - var b2 = parseInt(c2.substring(5, 7), 16); - var r = Math.round(r1 * (1 - ratio) + r2 * ratio); - var g = Math.round(g1 * (1 - ratio) + g2 * ratio); - var b = Math.round(b1 * (1 - ratio) + b2 * ratio); - r = ('0' + (r || 0).toString(16)).slice(-2); - g = ('0' + (g || 0).toString(16)).slice(-2); - b = ('0' + (b || 0).toString(16)).slice(-2); - return '#' + r + g + b; -} -`, - ); - const code = functionName + '(' + c1 + ', ' + c2 + ', ' + ratio + ')'; - return [code, Order.FUNCTION_CALL]; -} diff --git a/generators/javascript/text.ts b/generators/javascript/text.ts index 3973bd802..d31bdcf4b 100644 --- a/generators/javascript/text.ts +++ b/generators/javascript/text.ts @@ -66,16 +66,6 @@ export function text( return [code, Order.ATOMIC]; } -export function text_multiline( - block: Block, - generator: JavascriptGenerator, -): [string, Order] { - // Text value. - const code = generator.multiline_quote_(block.getFieldValue('TEXT')); - const order = code.includes('+') ? Order.ADDITION : Order.ATOMIC; - return [code, order]; -} - export function text_join( block: Block, generator: JavascriptGenerator, diff --git a/generators/lua.ts b/generators/lua.ts index 5c5b0af00..0cf81fb96 100644 --- a/generators/lua.ts +++ b/generators/lua.ts @@ -12,7 +12,6 @@ // Former goog.module ID: Blockly.Lua.all import {LuaGenerator} from './lua/lua_generator.js'; -import * as colour from './lua/colour.js'; import * as lists from './lua/lists.js'; import * as logic from './lua/logic.js'; import * as loops from './lua/loops.js'; @@ -31,7 +30,6 @@ export const luaGenerator = new LuaGenerator(); // Install per-block-type generator functions: const generators: typeof luaGenerator.forBlock = { - ...colour, ...lists, ...logic, ...loops, diff --git a/generators/lua/colour.ts b/generators/lua/colour.ts deleted file mode 100644 index f4a6a8ea9..000000000 --- a/generators/lua/colour.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * @license - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @file Generating Lua for colour blocks. - */ - -// Former goog.module ID: Blockly.Lua.colour - -import type {Block} from '../../core/block.js'; -import type {LuaGenerator} from './lua_generator.js'; -import {Order} from './lua_generator.js'; - -export function colour_picker( - block: Block, - generator: LuaGenerator, -): [string, Order] { - // Colour picker. - const code = generator.quote_(block.getFieldValue('COLOUR')); - return [code, Order.ATOMIC]; -} - -export function colour_random( - block: Block, - generator: LuaGenerator, -): [string, Order] { - // Generate a random colour. - const code = 'string.format("#%06x", math.random(0, 2^24 - 1))'; - return [code, Order.HIGH]; -} - -export function colour_rgb( - block: Block, - generator: LuaGenerator, -): [string, Order] { - // Compose a colour from RGB components expressed as percentages. - const functionName = generator.provideFunction_( - 'colour_rgb', - ` -function ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b) - r = math.floor(math.min(100, math.max(0, r)) * 2.55 + .5) - g = math.floor(math.min(100, math.max(0, g)) * 2.55 + .5) - b = math.floor(math.min(100, math.max(0, b)) * 2.55 + .5) - return string.format("#%02x%02x%02x", r, g, b) -end -`, - ); - const r = generator.valueToCode(block, 'RED', Order.NONE) || 0; - const g = generator.valueToCode(block, 'GREEN', Order.NONE) || 0; - const b = generator.valueToCode(block, 'BLUE', Order.NONE) || 0; - const code = functionName + '(' + r + ', ' + g + ', ' + b + ')'; - return [code, Order.HIGH]; -} - -export function colour_blend( - block: Block, - generator: LuaGenerator, -): [string, Order] { - // Blend two colours together. - const functionName = generator.provideFunction_( - 'colour_blend', - ` -function ${generator.FUNCTION_NAME_PLACEHOLDER_}(colour1, colour2, ratio) - local r1 = tonumber(string.sub(colour1, 2, 3), 16) - local r2 = tonumber(string.sub(colour2, 2, 3), 16) - local g1 = tonumber(string.sub(colour1, 4, 5), 16) - local g2 = tonumber(string.sub(colour2, 4, 5), 16) - local b1 = tonumber(string.sub(colour1, 6, 7), 16) - local b2 = tonumber(string.sub(colour2, 6, 7), 16) - local ratio = math.min(1, math.max(0, ratio)) - local r = math.floor(r1 * (1 - ratio) + r2 * ratio + .5) - local g = math.floor(g1 * (1 - ratio) + g2 * ratio + .5) - local b = math.floor(b1 * (1 - ratio) + b2 * ratio + .5) - return string.format("#%02x%02x%02x", r, g, b) -end -`, - ); - const colour1 = - generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; - const colour2 = - generator.valueToCode(block, 'COLOUR2', Order.NONE) || "'#000000'"; - const ratio = generator.valueToCode(block, 'RATIO', Order.NONE) || 0; - const code = - functionName + '(' + colour1 + ', ' + colour2 + ', ' + ratio + ')'; - return [code, Order.HIGH]; -} diff --git a/generators/lua/lists.ts b/generators/lua/lists.ts index 1af18b955..901ed88a6 100644 --- a/generators/lua/lists.ts +++ b/generators/lua/lists.ts @@ -33,7 +33,7 @@ export function lists_create_with( const elements = new Array(createWithBlock.itemCount_); for (let i = 0; i < createWithBlock.itemCount_; i++) { elements[i] = - generator.valueToCode(createWithBlock, 'ADD' + i, Order.NONE) || 'None'; + generator.valueToCode(createWithBlock, 'ADD' + i, Order.NONE) || 'nil'; } const code = '{' + elements.join(', ') + '}'; return [code, Order.HIGH]; @@ -56,7 +56,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(item, count) end `, ); - const element = generator.valueToCode(block, 'ITEM', Order.NONE) || 'None'; + const element = generator.valueToCode(block, 'ITEM', Order.NONE) || 'nil'; const repeatCount = generator.valueToCode(block, 'NUM', Order.NONE) || '0'; const code = functionName + '(' + element + ', ' + repeatCount + ')'; return [code, Order.HIGH]; @@ -258,7 +258,7 @@ export function lists_setIndex(block: Block, generator: LuaGenerator): string { const mode = block.getFieldValue('MODE') || 'SET'; const where = block.getFieldValue('WHERE') || 'FROM_START'; const at = generator.valueToCode(block, 'AT', Order.ADDITIVE) || '1'; - const value = generator.valueToCode(block, 'TO', Order.NONE) || 'None'; + const value = generator.valueToCode(block, 'TO', Order.NONE) || 'Nil'; let code = ''; // If `list` would be evaluated more than once (which is the case for LAST, diff --git a/generators/lua/text.ts b/generators/lua/text.ts index 350b86313..1c4a79a8e 100644 --- a/generators/lua/text.ts +++ b/generators/lua/text.ts @@ -21,16 +21,6 @@ export function text(block: Block, generator: LuaGenerator): [string, Order] { return [code, Order.ATOMIC]; } -export function text_multiline( - block: Block, - generator: LuaGenerator, -): [string, Order] { - // Text value. - const code = generator.multiline_quote_(block.getFieldValue('TEXT')); - const order = code.includes('..') ? Order.CONCATENATION : Order.ATOMIC; - return [code, order]; -} - export function text_join( block: Block, generator: LuaGenerator, diff --git a/generators/php.ts b/generators/php.ts index 18631ad9a..30a080866 100644 --- a/generators/php.ts +++ b/generators/php.ts @@ -13,7 +13,6 @@ // Former goog.module ID: Blockly.PHP.all import {PhpGenerator} from './php/php_generator.js'; -import * as colour from './php/colour.js'; import * as lists from './php/lists.js'; import * as logic from './php/logic.js'; import * as loops from './php/loops.js'; @@ -33,7 +32,6 @@ export const phpGenerator = new PhpGenerator(); // Install per-block-type generator functions: const generators: typeof phpGenerator.forBlock = { - ...colour, ...lists, ...logic, ...loops, diff --git a/generators/php/colour.ts b/generators/php/colour.ts deleted file mode 100644 index eefb7cba7..000000000 --- a/generators/php/colour.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * @license - * Copyright 2015 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @file Generating PHP for colour blocks. - */ - -// Former goog.module ID: Blockly.PHP.colour - -import type {Block} from '../../core/block.js'; -import {Order} from './php_generator.js'; -import type {PhpGenerator} from './php_generator.js'; - -export function colour_picker( - block: Block, - generator: PhpGenerator, -): [string, Order] { - // Colour picker. - const code = generator.quote_(block.getFieldValue('COLOUR')); - return [code, Order.ATOMIC]; -} - -export function colour_random( - block: Block, - generator: PhpGenerator, -): [string, Order] { - // Generate a random colour. - const functionName = generator.provideFunction_( - 'colour_random', - ` -function ${generator.FUNCTION_NAME_PLACEHOLDER_}() { - return '#' . str_pad(dechex(mt_rand(0, 0xFFFFFF)), 6, '0', STR_PAD_LEFT); -} -`, - ); - const code = functionName + '()'; - return [code, Order.FUNCTION_CALL]; -} - -export function colour_rgb( - block: Block, - generator: PhpGenerator, -): [string, Order] { - // Compose a colour from RGB components expressed as percentages. - const red = generator.valueToCode(block, 'RED', Order.NONE) || 0; - const green = generator.valueToCode(block, 'GREEN', Order.NONE) || 0; - const blue = generator.valueToCode(block, 'BLUE', Order.NONE) || 0; - const functionName = generator.provideFunction_( - 'colour_rgb', - ` -function ${generator.FUNCTION_NAME_PLACEHOLDER_}($r, $g, $b) { - $r = round(max(min($r, 100), 0) * 2.55); - $g = round(max(min($g, 100), 0) * 2.55); - $b = round(max(min($b, 100), 0) * 2.55); - $hex = '#'; - $hex .= str_pad(dechex($r), 2, '0', STR_PAD_LEFT); - $hex .= str_pad(dechex($g), 2, '0', STR_PAD_LEFT); - $hex .= str_pad(dechex($b), 2, '0', STR_PAD_LEFT); - return $hex; -} -`, - ); - const code = functionName + '(' + red + ', ' + green + ', ' + blue + ')'; - return [code, Order.FUNCTION_CALL]; -} - -export function colour_blend( - block: Block, - generator: PhpGenerator, -): [string, Order] { - // Blend two colours together. - const c1 = generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; - const c2 = generator.valueToCode(block, 'COLOUR2', Order.NONE) || "'#000000'"; - const ratio = generator.valueToCode(block, 'RATIO', Order.NONE) || 0.5; - const functionName = generator.provideFunction_( - 'colour_blend', - ` -function ${generator.FUNCTION_NAME_PLACEHOLDER_}($c1, $c2, $ratio) { - $ratio = max(min($ratio, 1), 0); - $r1 = hexdec(substr($c1, 1, 2)); - $g1 = hexdec(substr($c1, 3, 2)); - $b1 = hexdec(substr($c1, 5, 2)); - $r2 = hexdec(substr($c2, 1, 2)); - $g2 = hexdec(substr($c2, 3, 2)); - $b2 = hexdec(substr($c2, 5, 2)); - $r = round($r1 * (1 - $ratio) + $r2 * $ratio); - $g = round($g1 * (1 - $ratio) + $g2 * $ratio); - $b = round($b1 * (1 - $ratio) + $b2 * $ratio); - $hex = '#'; - $hex .= str_pad(dechex($r), 2, '0', STR_PAD_LEFT); - $hex .= str_pad(dechex($g), 2, '0', STR_PAD_LEFT); - $hex .= str_pad(dechex($b), 2, '0', STR_PAD_LEFT); - return $hex; -} -`, - ); - const code = functionName + '(' + c1 + ', ' + c2 + ', ' + ratio + ')'; - return [code, Order.FUNCTION_CALL]; -} diff --git a/generators/php/text.ts b/generators/php/text.ts index 86627653b..811e0251f 100644 --- a/generators/php/text.ts +++ b/generators/php/text.ts @@ -21,16 +21,6 @@ export function text(block: Block, generator: PhpGenerator): [string, Order] { return [code, Order.ATOMIC]; } -export function text_multiline( - block: Block, - generator: PhpGenerator, -): [string, Order] { - // Text value. - const code = generator.multiline_quote_(block.getFieldValue('TEXT')); - const order = code.includes('.') ? Order.STRING_CONCAT : Order.ATOMIC; - return [code, order]; -} - export function text_join( block: Block, generator: PhpGenerator, diff --git a/generators/python.ts b/generators/python.ts index 08ab10e81..d7b505763 100644 --- a/generators/python.ts +++ b/generators/python.ts @@ -13,7 +13,6 @@ // Former goog.module ID: Blockly.Python.all import {PythonGenerator} from './python/python_generator.js'; -import * as colour from './python/colour.js'; import * as lists from './python/lists.js'; import * as logic from './python/logic.js'; import * as loops from './python/loops.js'; @@ -38,7 +37,6 @@ pythonGenerator.addReservedWords('math,random,Number'); // Install per-block-type generator functions: // Install per-block-type generator functions: const generators: typeof pythonGenerator.forBlock = { - ...colour, ...lists, ...logic, ...loops, diff --git a/generators/python/colour.ts b/generators/python/colour.ts deleted file mode 100644 index 729d87cf5..000000000 --- a/generators/python/colour.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @file Generating Python for colour blocks. - */ - -// Former goog.module ID: Blockly.Python.colour - -import type {Block} from '../../core/block.js'; -import type {PythonGenerator} from './python_generator.js'; -import {Order} from './python_generator.js'; - -export function colour_picker( - block: Block, - generator: PythonGenerator, -): [string, Order] { - // Colour picker. - const code = generator.quote_(block.getFieldValue('COLOUR')); - return [code, Order.ATOMIC]; -} - -export function colour_random( - block: Block, - generator: PythonGenerator, -): [string, Order] { - // Generate a random colour. - // TODO(#7600): find better approach than casting to any to override - // CodeGenerator declaring .definitions protected. - (generator as AnyDuringMigration).definitions_['import_random'] = - 'import random'; - const code = "'#%06x' % random.randint(0, 2**24 - 1)"; - return [code, Order.FUNCTION_CALL]; -} - -export function colour_rgb( - block: Block, - generator: PythonGenerator, -): [string, Order] { - // Compose a colour from RGB components expressed as percentages. - const functionName = generator.provideFunction_( - 'colour_rgb', - ` -def ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b): - r = round(min(100, max(0, r)) * 2.55) - g = round(min(100, max(0, g)) * 2.55) - b = round(min(100, max(0, b)) * 2.55) - return '#%02x%02x%02x' % (r, g, b) -`, - ); - const r = generator.valueToCode(block, 'RED', Order.NONE) || 0; - const g = generator.valueToCode(block, 'GREEN', Order.NONE) || 0; - const b = generator.valueToCode(block, 'BLUE', Order.NONE) || 0; - const code = functionName + '(' + r + ', ' + g + ', ' + b + ')'; - return [code, Order.FUNCTION_CALL]; -} - -export function colour_blend( - block: Block, - generator: PythonGenerator, -): [string, Order] { - // Blend two colours together. - const functionName = generator.provideFunction_( - 'colour_blend', - ` -def ${generator.FUNCTION_NAME_PLACEHOLDER_}(colour1, colour2, ratio): - r1, r2 = int(colour1[1:3], 16), int(colour2[1:3], 16) - g1, g2 = int(colour1[3:5], 16), int(colour2[3:5], 16) - b1, b2 = int(colour1[5:7], 16), int(colour2[5:7], 16) - ratio = min(1, max(0, ratio)) - r = round(r1 * (1 - ratio) + r2 * ratio) - g = round(g1 * (1 - ratio) + g2 * ratio) - b = round(b1 * (1 - ratio) + b2 * ratio) - return '#%02x%02x%02x' % (r, g, b) -`, - ); - const colour1 = - generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; - const colour2 = - generator.valueToCode(block, 'COLOUR2', Order.NONE) || "'#000000'"; - const ratio = generator.valueToCode(block, 'RATIO', Order.NONE) || 0; - const code = - functionName + '(' + colour1 + ', ' + colour2 + ', ' + ratio + ')'; - return [code, Order.FUNCTION_CALL]; -} diff --git a/generators/python/text.ts b/generators/python/text.ts index b549158ed..e9154da83 100644 --- a/generators/python/text.ts +++ b/generators/python/text.ts @@ -26,16 +26,6 @@ export function text( return [code, Order.ATOMIC]; } -export function text_multiline( - block: Block, - generator: PythonGenerator, -): [string, Order] { - // Text value. - const code = generator.multiline_quote_(block.getFieldValue('TEXT')); - const order = code.includes('+') ? Order.ADDITIVE : Order.ATOMIC; - return [code, order]; -} - /** * Regular expression to detect a single-quoted string literal. */ diff --git a/media/delete-icon.svg b/media/delete-icon.svg new file mode 100644 index 000000000..1bd27ee73 --- /dev/null +++ b/media/delete-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/media/foldout-icon.svg b/media/foldout-icon.svg new file mode 100644 index 000000000..7aeb5b174 --- /dev/null +++ b/media/foldout-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/media/resize-handle.svg b/media/resize-handle.svg new file mode 100644 index 000000000..4002304e2 --- /dev/null +++ b/media/resize-handle.svg @@ -0,0 +1,3 @@ + + + diff --git a/msg/json/en.json b/msg/json/en.json index 38e53c8ad..50800bc27 100644 --- a/msg/json/en.json +++ b/msg/json/en.json @@ -370,6 +370,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "do something", "PROCEDURES_BEFORE_PARAMS": "with:", "PROCEDURES_CALL_BEFORE_PARAMS": "with:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Can't run the user-defined function '%1' because the definition block is disabled.", "PROCEDURES_DEFNORETURN_DO": "", "PROCEDURES_DEFNORETURN_TOOLTIP": "Creates a function with no output.", "PROCEDURES_DEFNORETURN_COMMENT": "Describe this function...", diff --git a/msg/json/qqq.json b/msg/json/qqq.json index e11c65f5d..fcd8897bd 100644 --- a/msg/json/qqq.json +++ b/msg/json/qqq.json @@ -376,6 +376,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "default name - This acts as a placeholder for the name of a function on a function definition block, as shown on [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#w7cfju this block]. The user will replace it with the function's name.", "PROCEDURES_BEFORE_PARAMS": "block text - This precedes the list of parameters on a function's definition block. See [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#voztpd this sample function with parameters].", "PROCEDURES_CALL_BEFORE_PARAMS": "block text - This precedes the list of parameters on a function's caller block. See [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#voztpd this sample function with parameters].", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "warning - This appears if a block that runs a function can't run because the function definition block is disabled. See [https://blockly-demo.appspot.com/static/demos/code/index.html#q947d7 this sample of a disabled function definition and call block].", "PROCEDURES_DEFNORETURN_DO": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - This appears next to the function's 'body', the blocks that should be run when the function is called, as shown in [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#voztpd this sample function definition].", "PROCEDURES_DEFNORETURN_TOOLTIP": "tooltip", "PROCEDURES_DEFNORETURN_COMMENT": "Placeholder text that the user is encouraged to replace with a description of what their function does.", diff --git a/msg/messages.js b/msg/messages.js index 153df1565..6b9d663a6 100644 --- a/msg/messages.js +++ b/msg/messages.js @@ -1491,6 +1491,12 @@ Blockly.Msg.PROCEDURES_BEFORE_PARAMS = 'with:'; /// function with parameters]. Blockly.Msg.PROCEDURES_CALL_BEFORE_PARAMS = 'with:'; /** @type {string} */ +/// warning - This appears if a block that runs a function can't run because the function +/// definition block is disabled. See +/// [https://blockly-demo.appspot.com/static/demos/code/index.html#q947d7 this sample of a +/// disabled function definition and call block]. +Blockly.Msg.PROCEDURES_CALL_DISABLED_DEF_WARNING = 'Can\'t run the user-defined function "%1" because the definition block is disabled.'; +/** @type {string} */ /// {{Optional|Supply translation only if your language requires it. Most do not.}} /// block text - This appears next to the function's "body", the blocks that should be /// run when the function is called, as shown in diff --git a/package-lock.json b/package-lock.json index 1b8f96d73..362504991 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "blockly", - "version": "10.4.3", + "version": "11.0.0-beta.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "10.4.3", + "version": "11.0.0-beta.10", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "jsdom": "22.1.0" + "jsdom": "23.0.0" }, "devDependencies": { "@blockly/block-test": "^5.0.0", @@ -51,6 +51,9 @@ "typescript": "^5.3.3", "webdriverio": "^8.32.2", "yargs": "^17.2.1" + }, + "engines": { + "node": ">=18" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1007,6 +1010,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "peer": true, "engines": { "node": ">= 10" } @@ -1483,18 +1488,6 @@ "node": ">=16.3.0" } }, - "node_modules/@wdio/utils/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@wdio/utils/node_modules/decamelize": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", @@ -1570,7 +1563,10 @@ "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "peer": true }, "node_modules/acorn": { "version": "8.11.2", @@ -1594,14 +1590,14 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", "dependencies": { - "debug": "4" + "debug": "^4.3.4" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/ajv": { @@ -2240,6 +2236,166 @@ "jsdom": "22.1.0" } }, + "node_modules/blockly/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/blockly/node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "peer": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/blockly/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "peer": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/blockly/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/blockly/node_modules/jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "peer": true, + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/blockly/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "peer": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/blockly/node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "peer": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/blockly/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/blockly/node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "peer": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/blockly/node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, "node_modules/bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", @@ -3054,6 +3210,17 @@ "node": ">=0.10.0" } }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", @@ -3079,6 +3246,41 @@ "node": ">= 14" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -3346,6 +3548,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "peer": true, "dependencies": { "webidl-conversions": "^7.0.0" }, @@ -4464,9 +4669,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "dev": true, "funding": [ { @@ -4718,18 +4923,6 @@ "node": "^16.13 || >=18 || >=20" } }, - "node_modules/geckodriver/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/geckodriver/node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -6222,6 +6415,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, "dependencies": { "whatwg-encoding": "^2.0.0" }, @@ -6250,16 +6444,15 @@ } }, "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/http-server": { @@ -6303,15 +6496,26 @@ } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", "dependencies": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/ieee754": { @@ -6848,39 +7052,37 @@ } }, "node_modules/jsdom": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", - "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.0.0.tgz", + "integrity": "sha512-cbL/UCtohJguhFC7c2/hgW6BeZCNvP7URQGnx9tSJRYKCdnfbfWOrtuLTMfiB2VxKsx5wPHVsh/J0aBy9lIIhQ==", "dependencies": { - "abab": "^2.0.6", "cssstyle": "^3.0.0", - "data-urls": "^4.0.0", + "data-urls": "^5.0.0", "decimal.js": "^10.4.3", - "domexception": "^4.0.0", "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.4", + "nwsapi": "^2.2.7", "parse5": "^7.1.2", "rrweb-cssom": "^0.6.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", + "tough-cookie": "^4.1.3", + "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.1", - "ws": "^8.13.0", - "xml-name-validator": "^4.0.0" + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.14.2", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -6888,51 +7090,69 @@ } } }, - "node_modules/jsdom/node_modules/cssstyle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", - "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", - "dependencies": { - "rrweb-cssom": "^0.6.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/jsdom/node_modules/data-urls": { + "node_modules/jsdom/node_modules/html-encoding-sniffer": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", - "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/jsdom/node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", "dependencies": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=14" + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" } }, "node_modules/jsdom/node_modules/whatwg-url": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", - "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", "dependencies": { - "tr46": "^4.1.1", + "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/json-buffer": { @@ -8064,9 +8284,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.5.tgz", - "integrity": "sha512-6xpotnECFy/og7tKSBVmUNft7J3jyXAka4XvG6AUhFWRz+Q/Ljus7znJAA3bxColfQLdS+XsjoodtJfCgeTEFQ==" + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" }, "node_modules/object-assign": { "version": "4.1.1", @@ -8408,44 +8628,6 @@ "node": ">= 14" } }, - "node_modules/pac-proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", - "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/pac-resolver": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.0.tgz", @@ -8948,44 +9130,6 @@ "node": ">= 14" } }, - "node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", - "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/proxy-agent/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -9038,9 +9182,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "engines": { "node": ">=6" } @@ -10026,18 +10170,6 @@ "node": ">= 14" } }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/socks/node_modules/ip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", @@ -11248,14 +11380,14 @@ } }, "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dependencies": { - "xml-name-validator": "^4.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/wait-port": { @@ -11397,6 +11529,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, "dependencies": { "iconv-lite": "0.6.3" }, @@ -11404,23 +11537,12 @@ "node": ">=12" } }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-url": { @@ -11511,6 +11633,7 @@ "version": "8.13.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, "engines": { "node": ">=10.0.0" }, @@ -11528,11 +11651,11 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xmlchars": { diff --git a/package.json b/package.json index 2b9c602d4..82f701dea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "10.4.3", + "version": "11.0.0-beta.10", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly" @@ -54,14 +54,54 @@ "test:compile:advanced": "gulp buildAdvancedCompilationTest --debug", "updateGithubPages": "npm ci && gulp gitUpdateGithubPages" }, - "main": "./index.js", - "umd": "./blockly.min.js", - "unpkg": "./blockly.min.js", - "types": "./index.d.ts", - "browser": { - "./node.js": "./browser.js", - "./core.js": "./core-browser.js", - "./blockly-node.js": "./blockly.js" + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.mjs", + "umd": "./blockly.min.js", + "default": "./index.js" + }, + "./core": { + "types": "./core.d.ts", + "node": "./core-node.js", + "import": "./blockly.mjs", + "default": "./blockly_compressed.js" + }, + "./blocks": { + "types": "./blocks.d.ts", + "import": "./blocks.mjs", + "default": "./blocks_compressed.js" + }, + "./dart": { + "types": "./dart.d.ts", + "import": "./dart.mjs", + "default": "./dart_compressed.js" + }, + "./lua": { + "types": "./lua.d.ts", + "import": "./lua.mjs", + "default": "./lua_compressed.js" + }, + "./javascript": { + "types": "./javascript.d.ts", + "import": "./javascript.mjs", + "default": "./javascript_compressed.js" + }, + "./php": { + "types": "./php.d.ts", + "import": "./php.mjs", + "default": "./php_compressed.js" + }, + "./python": { + "types": "./python.d.ts", + "import": "./python.mjs", + "default": "./python_compressed.js" + }, + "./msg/*": { + "types": "./msg/*.d.ts", + "import": "./msg/*.mjs", + "default": "./msg/*.js" + } }, "license": "Apache-2.0", "devDependencies": { @@ -105,6 +145,9 @@ "yargs": "^17.2.1" }, "dependencies": { - "jsdom": "22.1.0" + "jsdom": "23.0.0" + }, + "engines": { + "node": ">=18" } } diff --git a/scripts/gulpfiles/build_tasks.js b/scripts/gulpfiles/build_tasks.js index 8775f6918..d7b118f32 100644 --- a/scripts/gulpfiles/build_tasks.js +++ b/scripts/gulpfiles/build_tasks.js @@ -23,7 +23,7 @@ const closureCompiler = require('google-closure-compiler').gulp(); const argv = require('yargs').argv; const {rimraf} = require('rimraf'); -const {BUILD_DIR, RELEASE_DIR, TSC_OUTPUT_DIR, TYPINGS_BUILD_DIR} = require('./config'); +const {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TSC_OUTPUT_DIR, TYPINGS_BUILD_DIR} = require('./config'); const {getPackageJson} = require('./helper_tasks'); const {posixPath, quote} = require('../helpers'); @@ -344,28 +344,43 @@ this removal! done(); } +var languages = null; + +/** + * Get list of languages to build langfiles and/or shims for, based on .json + * files in msg/json/, skipping certain entries that do not correspond to an + * actual language). Results are cached as this is called from both + * buildLangfiles and buildLangfileShims. + */ +function getLanguages() { + if (!languages) { + const skip = /^(keys|synonyms|qqq|constants)\.json$/; + languages = fs.readdirSync(path.join('msg', 'json')) + .filter(file => file.endsWith('json') && !skip.test(file)) + .map(file => file.replace(/\.json$/, '')); + } + return languages; +} + /** * This task builds Blockly's lang files. * msg/*.js */ function buildLangfiles(done) { // Create output directory. - const outputDir = path.join(BUILD_DIR, 'msg'); - fs.mkdirSync(outputDir, {recursive: true}); + fs.mkdirSync(LANG_BUILD_DIR, {recursive: true}); // Run create_messages.py. - let json_files = fs.readdirSync(path.join('msg', 'json')); - json_files = json_files.filter(file => file.endsWith('json') && - !(new RegExp(/(keys|synonyms|qqq|constants)\.json$/).test(file))); - json_files = json_files.map(file => path.join('msg', 'json', file)); + const inputFiles = getLanguages().map( + lang => path.join('msg', 'json', `${lang}.json`)); const createMessagesCmd = `${PYTHON} ./scripts/i18n/create_messages.py \ --source_lang_file ${path.join('msg', 'json', 'en.json')} \ --source_synonym_file ${path.join('msg', 'json', 'synonyms.json')} \ --source_constants_file ${path.join('msg', 'json', 'constants.json')} \ --key_file ${path.join('msg', 'json', 'keys.json')} \ - --output_dir ${outputDir} \ - --quiet ${json_files.join(' ')}`; + --output_dir ${LANG_BUILD_DIR} \ + --quiet ${inputFiles.join(' ')}`; execSync(createMessagesCmd, {stdio: 'inherit'}); done(); @@ -566,7 +581,10 @@ function buildCompiled() { } /** - * This task builds the shims used by the playgrounds and tests to + * This task builds the ESM wrappers used by the chunk "import" + * entrypoints declared in package.json. + * + * Also builds the shims used by the playgrounds and tests to * load Blockly in either compressed or uncompressed mode, creating * build/blockly.loader.mjs, blocks.loader.mjs, javascript.loader.mjs, * etc. @@ -580,11 +598,38 @@ async function buildShims() { const TMP_PACKAGE_JSON = path.join(BUILD_DIR, 'package.json'); await fsPromises.writeFile(TMP_PACKAGE_JSON, '{"type": "module"}'); - // Import each entrypoint module, enumerate its exports, and write - // a shim to load the chunk either by importing the entrypoint - // module or by loading the compiled script. await Promise.all(chunks.map(async (chunk) => { + // Import chunk entrypoint to get names of exports for chunk. const entryPath = path.posix.join(TSC_OUTPUT_DIR_POSIX, chunk.entry); + const exportedNames = Object.keys(await import(`../../${entryPath}`)); + + // Write an ESM wrapper that imports the CJS module and re-exports + // its named exports. + const cjsPath = `./${chunk.name}${COMPILED_SUFFIX}.js`; + const wrapperPath = path.join(RELEASE_DIR, `${chunk.name}.mjs`); + const importName = chunk.scriptExport.replace(/.*\./, ''); + + await fsPromises.writeFile(wrapperPath, + `import ${importName} from '${cjsPath}'; +export const { +${exportedNames.map((name) => ` ${name},`).join('\n')} +} = ${importName}; +`); + + // For first chunk, write an additional ESM wrapper for 'blockly' + // entrypoint since it has the same exports as 'blockly/core'. + if (chunk.name === 'blockly') { + await fsPromises.writeFile(path.join(RELEASE_DIR, `index.mjs`), + `import Blockly from './index.js'; +export const { +${exportedNames.map((name) => ` ${name},`).join('\n')} +} = Blockly; +`); + } + + // Write a loading shim that uses loadChunk to either import the + // chunk's entrypoint (e.g. build/src/core/blockly.js) or load the + // compressed chunk (e.g. dist/blockly_compressed.js) as a script. const scriptPath = path.posix.join(RELEASE_DIR, `${chunk.name}${COMPILED_SUFFIX}.js`); const shimPath = path.join(BUILD_DIR, `${chunk.name}.loader.mjs`); @@ -592,14 +637,13 @@ async function buildShims() { chunk.parent ? `import ${quote(`./${chunk.parent.name}.loader.mjs`)};` : ''; - const exports = await import(`../../${entryPath}`); await fsPromises.writeFile(shimPath, `import {loadChunk} from '../tests/scripts/load.mjs'; ${parentImport} export const { -${Object.keys(exports).map((name) => ` ${name},`).join('\n')} +${exportedNames.map((name) => ` ${name},`).join('\n')} } = await loadChunk( ${quote(entryPath)}, ${quote(scriptPath)}, @@ -611,7 +655,37 @@ ${Object.keys(exports).map((name) => ` ${name},`).join('\n')} await fsPromises.rm(TMP_PACKAGE_JSON); } +/** + * This task builds the ESM wrappers used by the langfiles "import" + * entrypoints declared in package.json. + */ +async function buildLangfileShims() { + // Create output directory. + fs.mkdirSync(path.join(RELEASE_DIR, 'msg'), {recursive: true}); + // Get the names of the exports from the langfile by require()ing + // msg/messages.js and letting it mutate the (global) Blockly.Msg. + // (We have to do it this way because messages.js is a script and + // not a CJS module with exports.) + globalThis.Blockly = {Msg: {}}; + require('../../msg/messages.js'); + const exportedNames = Object.keys(globalThis.Blockly.Msg); + delete globalThis.Blockly; + + await Promise.all(getLanguages().map(async (lang) => { + // Write an ESM wrapper that imports the CJS module and re-exports + // its named exports. + const cjsPath = `./${lang}.js`; + const wrapperPath = path.join(RELEASE_DIR, 'msg', `${lang}.mjs`); + + await fsPromises.writeFile(wrapperPath, + `import ${lang} from '${cjsPath}'; +export const { +${exportedNames.map((name) => ` ${name},`).join('\n')} +} = ${lang}; +`); + })); +} /** * This task builds Blockly core, blocks and generators together and uses @@ -663,7 +737,7 @@ function cleanBuildDir() { // Main sequence targets. Each should invoke any immediate prerequisite(s). exports.cleanBuildDir = cleanBuildDir; -exports.langfiles = buildLangfiles; // Build build/msg/*.js from msg/json/*. +exports.langfiles = gulp.parallel(buildLangfiles, buildLangfileShims); exports.tsc = buildJavaScript; exports.minify = gulp.series(exports.tsc, buildCompiled, buildShims); exports.build = gulp.parallel(exports.minify, exports.langfiles); diff --git a/scripts/gulpfiles/config.js b/scripts/gulpfiles/config.js index e1756d96e..90cd57109 100644 --- a/scripts/gulpfiles/config.js +++ b/scripts/gulpfiles/config.js @@ -26,6 +26,9 @@ exports.BUILD_DIR = 'build'; // Directory to write typings output to. exports.TYPINGS_BUILD_DIR = path.join(exports.BUILD_DIR, 'declarations'); +// Directory to write langfile output to. +exports.LANG_BUILD_DIR = path.join(exports.BUILD_DIR, 'msg'); + // Directory where typescript compiler output can be found. // Matches the value in tsconfig.json: outDir exports.TSC_OUTPUT_DIR = path.join(exports.BUILD_DIR, 'src'); diff --git a/scripts/gulpfiles/package_tasks.js b/scripts/gulpfiles/package_tasks.js index 46d91ec5f..48dfd5b84 100644 --- a/scripts/gulpfiles/package_tasks.js +++ b/scripts/gulpfiles/package_tasks.js @@ -21,7 +21,7 @@ const fs = require('fs'); const {rimraf} = require('rimraf'); const build = require('./build_tasks'); const {getPackageJson} = require('./helper_tasks'); -const {BUILD_DIR, RELEASE_DIR, TYPINGS_BUILD_DIR} = require('./config'); +const {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TYPINGS_BUILD_DIR} = require('./config'); // Path to template files for gulp-umd. const TEMPLATE_DIR = 'scripts/package/templates'; @@ -40,244 +40,54 @@ function packageUMD(namespace, dependencies, template = 'umd.template') { }); }; -/** - * A helper method for wrapping a file into a CommonJS module for Node.js. - * @param {string} namespace The export namespace. - * @param {Array} dependencies An array of dependencies to inject. - */ -function packageCommonJS(namespace, dependencies) { - return gulp.umd({ - dependencies: function () { return dependencies; }, - namespace: function () { return namespace; }, - exports: function () { return namespace; }, - template: path.join(TEMPLATE_DIR, 'node.template') - }); -}; - -/** - * This task wraps scripts/package/blockly.js into a UMD module. - * @example import 'blockly/blockly'; - */ -function packageBlockly() { - return gulp.src('scripts/package/blockly.js') - .pipe(packageUMD('Blockly', [{ - name: 'Blockly', - amd: './blockly_compressed', - cjs: './blockly_compressed', - }])) - .pipe(gulp.rename('blockly.js')) - .pipe(gulp.dest(RELEASE_DIR)); -}; - -/** - * This task wraps scripts/package/blocks.js into a UMD module. - * @example import 'blockly/blocks'; - */ -function packageBlocks() { - return gulp.src('scripts/package/blocks.js') - .pipe(packageUMD('BlocklyBlocks', [{ - name: 'BlocklyBlocks', - amd: './blocks_compressed', - cjs: './blocks_compressed', - }])) - .pipe(gulp.rename('blocks.js')) - .pipe(gulp.dest(RELEASE_DIR)); -}; - /** * This task wraps scripts/package/index.js into a UMD module. - * We implicitly require the Node entry point in CommonJS environments, - * and the Browser entry point for AMD environments. - * @example import * as Blockly from 'blockly'; + * + * This module is the main entrypoint for the blockly package, and + * loads blockly/core, blockly/blocks and blockly/msg/en and then + * calls setLocale(en). */ function packageIndex() { return gulp.src('scripts/package/index.js') .pipe(packageUMD('Blockly', [{ name: 'Blockly', - amd: './browser', - cjs: './node', + amd: 'blockly/core', + cjs: 'blockly/core', + },{ + name: 'en', + amd: 'blockly/msg/en', + cjs: 'blockly/msg/en', + global: 'Blockly.Msg', + },{ + name: 'blocks', + amd: 'blockly/blocks', + cjs: 'blockly/blocks', + global: 'Blockly.Blocks', }])) - .pipe(gulp.rename('index.js')) .pipe(gulp.dest(RELEASE_DIR)); }; /** - * This task wraps scripts/package/browser/index.js into a UMD module. - * By default, the module includes Blockly core and built-in blocks, - * as well as the JavaScript code generator and the English block - * localization files. - * This module is configured (in package.json) to replaces the module - * built by package-node in browser environments. - * @example import * as Blockly from 'blockly/browser'; + * This task copies scripts/package/core-node.js into into the + * package. This module will be the 'blockly/core' entrypoint for + * node.js environments. + * + * Note that, unlike index.js, this file does not get a UMD wrapper. + * This is because it is only used in node.js environments and so is + * guaranteed to be loaded as a CJS module. */ -function packageBrowser() { - return gulp.src('scripts/package/browser/index.js') - .pipe(packageUMD('Blockly', [{ - name: 'Blockly', - amd: './core-browser', - cjs: './core-browser', - },{ - name: 'En', - amd: './msg/en', - cjs: './msg/en', - },{ - name: 'BlocklyBlocks', - amd: './blocks', - cjs: './blocks', - },{ - name: 'BlocklyJS', - amd: './javascript', - cjs: './javascript', - }])) - .pipe(gulp.rename('browser.js')) +function packageCoreNode() { + return gulp.src('scripts/package/core-node.js') .pipe(gulp.dest(RELEASE_DIR)); }; -/** - * This task wraps scripts/package/browser/core.js into a UMD module. - * By default, the module includes the Blockly core package and a - * helper method to set the locale. - * This module is configured (in package.json) to replaces the module - * built by package-node-core in browser environments. - * @example import * as Blockly from 'blockly/core'; - */ -function packageCore() { - return gulp.src('scripts/package/browser/core.js') - .pipe(packageUMD('Blockly', [{ - name: 'Blockly', - amd: './blockly', - cjs: './blockly', - }])) - .pipe(gulp.rename('core-browser.js')) - .pipe(gulp.dest(RELEASE_DIR)); -}; - -/** - * This task wraps scripts/package/node/index.js into a CommonJS module for Node.js. - * By default, the module includes Blockly core and built-in blocks, - * as well as all the code generators and the English block localization files. - * This module is configured (in package.json) to be replaced by the module - * built by package-browser in browser environments. - * @example import * as Blockly from 'blockly/node'; - */ -function packageNode() { - return gulp.src('scripts/package/node/index.js') - .pipe(packageCommonJS('Blockly', [{ - name: 'Blockly', - cjs: './core', - },{ - name: 'En', - cjs: './msg/en', - },{ - name: 'BlocklyBlocks', - cjs: './blocks', - },{ - name: 'BlocklyJS', - cjs: './javascript', - },{ - name: 'BlocklyPython', - cjs: './python', - },{ - name: 'BlocklyPHP', - cjs: './php', - },{ - name: 'BlocklyLua', - cjs: './lua', - }, { - name: 'BlocklyDart', - cjs: './dart', - }])) - .pipe(gulp.rename('node.js')) - .pipe(gulp.dest(RELEASE_DIR)); -}; - -/** - * This task wraps scripts/package/node/core.js into a CommonJS module for Node.js. - * By default, the module includes the Blockly core package for Node.js - * and a helper method to set the locale. - * This module is configured (in package.json) to be replaced by the module - * built by package-core in browser environments. - * @example import * as Blockly from 'blockly/core'; - */ -function packageNodeCore() { - return gulp.src('scripts/package/node/core.js') - .pipe(packageCommonJS('Blockly', [{ - name: 'Blockly', - amd: './blockly', - cjs: './blockly', - }])) - .pipe(gulp.rename('core.js')) - .pipe(gulp.dest(RELEASE_DIR)); -}; - -/** - * A helper method for wrapping a generator file into a UMD module. - * @param {string} file Source file name. - * @param {string} rename Destination file name. - * @param {string} namespace Export namespace. - */ -function packageGenerator(file, rename, namespace) { - return gulp.src(`scripts/package/${rename}`) - .pipe(packageUMD(`Blockly${namespace}`, [{ - name: 'Blockly', - amd: './core', - cjs: './core', - }, { - name: `Blockly${namespace}`, - amd: `./${file}`, - cjs: `./${file}`, - }])) - .pipe(gulp.rename(rename)) - .pipe(gulp.dest(RELEASE_DIR)); -}; - -/** - * This task wraps javascript_compressed.js into a UMD module. - * @example import 'blockly/javascript'; - */ -function packageJavascript() { - return packageGenerator('javascript_compressed.js', 'javascript.js', 'JavaScript'); -}; - -/** - * This task wraps python_compressed.js into a UMD module. - * @example import 'blockly/python'; - */ -function packagePython() { - return packageGenerator('python_compressed.js', 'python.js', 'Python'); -}; - -/** - * This task wraps lua_compressed.js into a UMD module. - * @example import 'blockly/lua'; - */ -function packageLua() { - return packageGenerator('lua_compressed.js', 'lua.js', 'Lua'); -}; - -/** - * This task wraps dart_compressed.js into a UMD module. - * @example import 'blockly/dart'; - */ -function packageDart() { - return packageGenerator('dart_compressed.js', 'dart.js', 'Dart'); -}; - -/** - * This task wraps php_compressed.js into a UMD module. - * @example import 'blockly/php'; - */ -function packagePHP() { - return packageGenerator('php_compressed.js', 'php.js', 'PHP'); -}; - /** * This task wraps each of the files in ${BUILD_DIR/msg/ into a UMD module. * @example import * as En from 'blockly/msg/en'; */ function packageLocales() { // Remove references to goog.provide and goog.require. - return gulp.src(`${BUILD_DIR}/msg/*.js`) + return gulp.src(`${LANG_BUILD_DIR}/*.js`) .pipe(gulp.replace(/goog\.[^\n]+/g, '')) .pipe(packageUMD('Blockly.Msg', [], 'umd-msg.template')) .pipe(gulp.dest(`${RELEASE_DIR}/msg`)); @@ -301,6 +111,50 @@ function packageUMDBundle() { .pipe(gulp.dest(`${RELEASE_DIR}`)); }; + +/** + * This task creates shims for the submodule entrypoints, for the + * benefit of bundlers and other build tools that do not correctly + * support the exports declaration in package.json. These shims just + * require() and reexport the corresponding *_compressed.js bundle. + * + * This should solve issues encountered by users of bundlers that don't + * support exports at all (e.g. browserify) as well as ones that don't + * support it in certain circumstances (e.g., when using webpack's + * resolve.alias configuration option to alias 'blockly' to + * 'node_modules/blockly', as we formerly did in most plugins, which + * causes webpack to ignore blockly's package.json entirely). + * + * Assumptions: + * - Such bundlers will _completely_ ignore the exports declaration. + * - The bundles are intended to be used in a browser—or at least not + * in node.js—so the core entrypoint never needs to route to + * core-node.js. This is reasonable since there's little reason to + * bundle code for node.js, and node.js has supported the exports + * clause since at least v12, consideably older than any version of + * node.js we officially support. + * - It suffices to provide only a CJS entrypoint (because we can only + * provide CJS or ESM, not both. (We could in future switch to + * providing only an ESM entrypoint instead, though.) + * + * @param {Function} done Callback to call when done. + */ +function packageLegacyEntrypoints(done) { + for (entrypoint of [ + 'core', 'blocks', 'dart', 'javascript', 'lua', 'php', 'python' + ]) { + const bundle = + (entrypoint === 'core' ? 'blockly' : entrypoint) + '_compressed.js'; + fs.writeFileSync(path.join(RELEASE_DIR, `${entrypoint}.js`), + `// Shim for backwards-compatibility with bundlers that do not +// support the 'exports' clause in package.json, to allow them +// to load the blockly/${entrypoint} submodule entrypoint. +module.exports = require('./${bundle}'); +`); + } + done(); +} + /** * This task copies all the media/* files into the release directory. */ @@ -310,18 +164,33 @@ function packageMedia() { }; /** - * This task copies the package.json file into the release directory. + * This task copies the package.json file into the release directory, + * with modifications: + * + * - The scripts section is removed. + * + * Prerequisite: buildLangfiles. + * + * @param {Function} done Callback to call when done. */ -function packageJSON(cb) { - const packageJson = getPackageJson(); - const json = Object.assign({}, packageJson); +function packageJSON(done) { + // Copy package.json, so we can safely modify it. + const json = JSON.parse(JSON.stringify(getPackageJson())); + // Remove unwanted entries. delete json['scripts']; + // Set "type": "commonjs", since that's what .js files in the + // package root are. This should be a no-op since that's the + // default, but by setting it explicitly we ensure that any chage to + // the repository top-level package.json to set "type": "module" + // won't break the published package accidentally. + json.type = 'commonjs'; + // Write resulting package.json file to release directory. if (!fs.existsSync(RELEASE_DIR)) { fs.mkdirSync(RELEASE_DIR, {recursive: true}); } fs.writeFileSync(`${RELEASE_DIR}/package.json`, JSON.stringify(json, null, 2)); - cb(); + done(); }; /** @@ -377,17 +246,8 @@ const package = gulp.series( build.build, gulp.parallel( packageIndex, - packageBrowser, - packageNode, - packageCore, - packageNodeCore, - packageBlockly, - packageBlocks, - packageJavascript, - packagePython, - packageLua, - packageDart, - packagePHP, + packageCoreNode, + packageLegacyEntrypoints, packageMedia, gulp.series(packageLocales, packageUMDBundle), packageJSON, diff --git a/scripts/package/blockly.js b/scripts/package/blockly.js deleted file mode 100644 index 77021b3d6..000000000 --- a/scripts/package/blockly.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Blockly module; just a wrapper for blockly_compressed.js. - */ diff --git a/scripts/package/blocks.js b/scripts/package/blocks.js deleted file mode 100644 index 66ae806bf..000000000 --- a/scripts/package/blocks.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Blockly blocks module; just a wrapper for blocks_compressed.js. - */ diff --git a/scripts/package/browser/core.js b/scripts/package/browser/core.js deleted file mode 100644 index b7adcc92a..000000000 --- a/scripts/package/browser/core.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Blockly core module for the browser. It includes blockly.js - * and adds a helper method for setting the locale. - */ - -/* eslint-disable */ -'use strict'; diff --git a/scripts/package/browser/index.js b/scripts/package/browser/index.js deleted file mode 100644 index 36d0c325b..000000000 --- a/scripts/package/browser/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Blockly module for the browser. This includes Blockly core - * and built in blocks, the JavaScript code generator and the English block - * localization files. - */ - -/* eslint-disable */ -'use strict'; - -// Include the EN Locale by default. -Blockly.setLocale(En); diff --git a/scripts/package/core-node.js b/scripts/package/core-node.js new file mode 100644 index 000000000..22c3c5bcd --- /dev/null +++ b/scripts/package/core-node.js @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file Blockly core module wrapper for node.js. This module loads + * blockly_compressed.js and jsdom, then calls + * Blockly.utils.xml.injectDependencies to supply needed XML-handling + * functions to Blocky. + * + * Note that, unlike index.js, this file does not get a UMD wrapper. + * This is because it is only used in node.js environments and so is + * guaranteed to be loaded as a CJS module. + */ + +/* eslint-disable */ +'use strict'; + +const Blockly = require('./blockly_compressed.js'); +const {JSDOM} = require('jsdom'); + +// Override textToDomDocument and provide node.js alternatives to +// DOMParser and XMLSerializer. +if (typeof globalThis.document !== 'object') { + const {window} = new JSDOM(``); + Blockly.utils.xml.injectDependencies(window); +} + +module.exports = Blockly; diff --git a/scripts/package/dart.js b/scripts/package/dart.js deleted file mode 100644 index d59060bde..000000000 --- a/scripts/package/dart.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Dart generator module; just a wrapper for dart_compressed.js. - */ diff --git a/scripts/package/index.js b/scripts/package/index.js index 0264fd446..8eb70b426 100644 --- a/scripts/package/index.js +++ b/scripts/package/index.js @@ -5,7 +5,19 @@ */ /** - * @fileoverview Blockly module; this is a wrapper which selects - * either browser.js or node.js, depending on which environment we - * are running in. + * @file Main entrypoint for blockly package. Via its UMD wrapper, + * this module loads blockly/core, blockly/blocks and blockly/msg/en + * and then calls setLocale(en). + * + * This entrypoint previously also loaded one or more generators + * (JavaScript in browser, all five in node.js environments) but it no + * longer makes sense to do so because of changes to generators + * exports (they no longer have the side effect of defining + * Blockly.JavaScript, etc., when loaded as modules). */ + +/* eslint-disable */ +'use strict'; + +// Include the EN Locale by default. +Blockly.setLocale(en); diff --git a/scripts/package/javascript.js b/scripts/package/javascript.js deleted file mode 100644 index e4adba09f..000000000 --- a/scripts/package/javascript.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview JavaScript Generator module; just a wrapper for - * javascript_compressed.js. - */ diff --git a/scripts/package/lua.js b/scripts/package/lua.js deleted file mode 100644 index ca37b44f9..000000000 --- a/scripts/package/lua.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Lua generator module; just a wrapper for lua_compressed.js. - */ diff --git a/scripts/package/node/core.js b/scripts/package/node/core.js deleted file mode 100644 index b789e1f31..000000000 --- a/scripts/package/node/core.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Blockly core module for Node. It includes blockly-node.js - * and adds a helper method for setting the locale. - */ - -/* eslint-disable */ -'use strict'; - -// Override textToDomDocument and provide Node.js alternatives to DOMParser and -// XMLSerializer. -if (typeof globalThis.document !== 'object') { - const {JSDOM} = require('jsdom'); - const {window} = new JSDOM(``); - Blockly.utils.xml.injectDependencies(window); -} diff --git a/scripts/package/node/index.js b/scripts/package/node/index.js deleted file mode 100644 index 33c322a98..000000000 --- a/scripts/package/node/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Blockly module for Node. It includes Blockly core, - * built-in blocks, all the generators and the English locale. - */ - -/* eslint-disable */ -'use strict'; - -// Include the EN Locale by default. -Blockly.setLocale(En); diff --git a/scripts/package/php.js b/scripts/package/php.js deleted file mode 100644 index a6ed3fa6d..000000000 --- a/scripts/package/php.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview PHP generator module; just a wrapper for php_compressed.js. - */ diff --git a/scripts/package/python.js b/scripts/package/python.js deleted file mode 100644 index 0bb64c06f..000000000 --- a/scripts/package/python.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Python generator module; just a wrapper for - * python_compressed.js. - */ diff --git a/scripts/package/templates/node.template b/scripts/package/templates/node.template deleted file mode 100644 index e9887243e..000000000 --- a/scripts/package/templates/node.template +++ /dev/null @@ -1,5 +0,0 @@ -/* eslint-disable */ -(function (<%= param %>){ -<%= contents %> -module.exports = <%= exports %>; -})(<%= cjs %>); diff --git a/tests/browser/test/workspace_comment_test.js b/tests/browser/test/workspace_comment_test.js new file mode 100644 index 000000000..b6adfebcd --- /dev/null +++ b/tests/browser/test/workspace_comment_test.js @@ -0,0 +1,224 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +const chai = require('chai'); +const sinon = require('sinon'); +const {Key} = require('webdriverio'); +const {testSetup, testFileLocations} = require('./test_setup'); + +suite('Workspace comments', function () { + // Setting timeout to unlimited as the webdriver takes a longer time + // to run than most mocha test + this.timeout(0); + + suiteSetup(async function () { + this.browser = await testSetup( + testFileLocations.PLAYGROUND + '?toolbox=test-blocks', + ); + }); + + teardown(async function () { + sinon.restore(); + + await this.browser.execute(() => { + Blockly.getMainWorkspace().clear(); + }); + }); + + async function createComment(browser) { + return await browser.execute(() => { + const comment = new Blockly.comments.RenderedWorkspaceComment( + Blockly.getMainWorkspace(), + ); + return comment.id; + }); + } + + async function hasClass(elem, className) { + return (await elem.getAttribute('class')).split(' ').includes(className); + } + + suite('Collapsing and uncollapsing', function () { + async function isCommentCollapsed(browser, id) { + return await browser.execute( + (id) => Blockly.getMainWorkspace().getCommentById(id).isCollapsed(), + id, + ); + } + + suite('Collapsing', function () { + test('collapsing updates the collapse value', async function () { + const commentId = await createComment(this.browser); + + const foldout = await this.browser.$( + '.blocklyComment .blocklyFoldoutIcon', + ); + await foldout.click(); + + chai.assert.isTrue( + await isCommentCollapsed(this.browser, commentId), + 'Expected the comment to be collapsed', + ); + }); + + test('collapsing adds the css class', async function () { + await createComment(this.browser); + + const foldout = await this.browser.$( + '.blocklyComment .blocklyFoldoutIcon', + ); + await foldout.click(); + + const comment = await this.browser.$('.blocklyComment'); + chai.assert.isTrue( + await hasClass(comment, 'blocklyCollapsed'), + 'Expected the comment to have the blocklyCollapsed class', + ); + }); + }); + + suite('Uncollapsing', function () { + async function collapseComment(browser, id) { + await browser.execute((id) => { + Blockly.getMainWorkspace().getCommentById(id).setCollapsed(true); + }, id); + } + + test('collapsing updates the collapse value', async function () { + const commentId = await createComment(this.browser); + await collapseComment(this.browser, commentId); + + const foldout = await this.browser.$( + '.blocklyComment .blocklyFoldoutIcon', + ); + await foldout.click(); + + chai.assert.isFalse( + await isCommentCollapsed(this.browser, commentId), + 'Expected the comment to not be collapsed', + ); + }); + + test('collapsing adds the css class', async function () { + const commentId = await createComment(this.browser); + await collapseComment(this.browser, commentId); + + const foldout = await this.browser.$( + '.blocklyComment .blocklyFoldoutIcon', + ); + await foldout.click(); + + const comment = await this.browser.$('.blocklyComment'); + chai.assert.isFalse( + await hasClass(comment, 'blocklyCollapsed'), + 'Expected the comment to not have the blocklyCollapsed class', + ); + }); + }); + }); + + suite('Deleting', function () { + async function makeDeleteVisible(browser, commentId) { + await browser.execute((id) => { + document.querySelector( + '.blocklyComment .blocklyDeleteIcon', + ).style.display = 'block'; + const comment = Blockly.getMainWorkspace().getCommentById(id); + comment.setSize(comment.getSize()); + }, commentId); + } + + async function commentIsDisposed(browser, commentId) { + return await browser.execute( + (id) => !Blockly.getMainWorkspace().getCommentById(id), + commentId, + ); + } + + test('deleting disposes of comment', async function () { + const commentId = await createComment(this.browser); + await makeDeleteVisible(this.browser, commentId); + + const deleteIcon = await this.browser.$( + '.blocklyComment .blocklyDeleteIcon', + ); + await deleteIcon.click(); + + chai.assert.isTrue( + await commentIsDisposed(this.browser, commentId), + 'Expected the comment model to be disposed', + ); + }); + + test('deleting disposes of DOM elements', async function () { + const commentId = await createComment(this.browser); + await makeDeleteVisible(this.browser, commentId); + + const deleteIcon = await this.browser.$( + '.blocklyComment .blocklyDeleteIcon', + ); + await deleteIcon.click(); + + chai.assert.isFalse( + await this.browser.$('.blocklyComment').isExisting(), + 'Expected the comment DOM elements to not exist', + ); + }); + }); + + suite('Typing', function () { + async function getCommentText(browser, id) { + return await browser.execute( + (id) => Blockly.getMainWorkspace().getCommentById(id).getText(), + id, + ); + } + + test('typing updates the text value', async function () { + const commentId = await createComment(this.browser); + + const textArea = await this.browser.$('.blocklyComment .blocklyTextarea'); + await textArea.addValue('test text'); + // Deselect text area to fire browser change event. + await this.browser.$('.blocklyWorkspace').click(); + + chai.assert.equal( + await getCommentText(this.browser, commentId), + 'test text', + 'Expected the comment model text to match the entered text', + ); + }); + }); + + suite('Resizing', function () { + async function getCommentSize(browser, id) { + return await browser.execute( + (id) => Blockly.getMainWorkspace().getCommentById(id).getSize(), + id, + ); + } + + test('resizing updates the size value', async function () { + const commentId = await createComment(this.browser); + const origSize = await getCommentSize(this.browser, commentId); + const delta = {x: 20, y: 20}; + + const resizeHandle = await this.browser.$( + '.blocklyComment .blocklyResizeHandle', + ); + await resizeHandle.dragAndDrop(delta); + + chai.assert.deepEqual( + await getCommentSize(this.browser, commentId), + { + width: origSize.width + delta.x, + height: origSize.height + delta.y, + }, + 'Expected the comment model size to match the resized size', + ); + }); + }); +}); diff --git a/tests/generators/colour.xml b/tests/generators/colour.xml deleted file mode 100644 index 05a9dee48..000000000 --- a/tests/generators/colour.xml +++ /dev/null @@ -1,318 +0,0 @@ - - - test colour picker - Describe this function... - - - - - static colour - - - - - #ff6600 - - - - - #ff6600 - - - - - - - test rgb - Describe this function... - - - - - from rgb - - - - - - - 100 - - - - - 40 - - - - - 0 - - - - - - - #ff6600 - - - - - - - Colour - - - - - - - - - - - - - - - - - - - - - - - test colour random - Describe this function... - - - - - 100 - - - - - item - - - - - - - - - - - length of random colour string: - - - - - item - - - - - - - - - item - - - - - - - 7 - - - - - - - - - - format of random colour string: - - - - - item - - - - - - - - FIRST - - - item - - - - - - - # - - - - - i - - - 1 - - - - - 6 - - - - - TRUE - - - - - - contents of random colour string: - - - - - item - - - - - at index: - - - - - - - i - - - - - - - - - NEQ - - - - - -1 - - - - - - - FIRST - - - abcdefABDEF0123456789 - - - - - - FROM_START - - - item - - - - - - - i - - - - - - - - - - - - - - - - - - - - - - - - - test blend - Describe this function... - - - - - blend - - - - - - - #ff0000 - - - - - - - 100 - - - - - 40 - - - - - 0 - - - - - - - 0.4 - - - - - - - #ff2900 - - - - - - diff --git a/tests/generators/golden/generated.dart b/tests/generators/golden/generated.dart index a47886588..f565ecae1 100644 --- a/tests/generators/golden/generated.dart +++ b/tests/generators/golden/generated.dart @@ -1,6 +1,6 @@ import 'dart:math' as Math; -var unittestResults, test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy; +var unittestResults, test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy; String unittest_report() { // Create test report. @@ -941,20 +941,6 @@ void test_replace() { unittest_assertequals(''.replaceAll('a', 'chicken'), '', 'empty source'); } -/// Tests the "multiline" block. -void test_multiline() { - unittest_assertequals('', '', 'no text'); - unittest_assertequals('Google', 'Google', 'simple'); - unittest_assertequals('paragraph' + '\n' + - 'with newlines' + '\n' + - 'yup', 'paragraph' + '\n' + - 'with newlines' + '\n' + - 'yup', 'no compile error with newlines'); - unittest_assertequals(text_count('bark bark' + '\n' + - 'bark bark bark' + '\n' + - 'bark bark bark bark', 'bark'), 9, 'count with newlines'); -} - /// Checks that the number of calls is one in order /// to confirm that a function was only called once. void check_number_of_calls2(test_name) { @@ -1426,80 +1412,6 @@ void test_lists_reverse() { unittest_assertequals(new List.from(list.reversed), [], 'empty list'); } -/// Describe this function... -void test_colour_picker() { - unittest_assertequals('#ff6600', '#ff6600', 'static colour'); -} - -String colour_rgb(num r, num g, num b) { - num rn = (Math.max(Math.min(r, 100), 0) * 2.55).round(); - String rs = rn.toInt().toRadixString(16); - rs = '0$rs'; - rs = rs.substring(rs.length - 2); - num gn = (Math.max(Math.min(g, 100), 0) * 2.55).round(); - String gs = gn.toInt().toRadixString(16); - gs = '0$gs'; - gs = gs.substring(gs.length - 2); - num bn = (Math.max(Math.min(b, 100), 0) * 2.55).round(); - String bs = bn.toInt().toRadixString(16); - bs = '0$bs'; - bs = bs.substring(bs.length - 2); - return '#$rs$gs$bs'; -} - -/// Describe this function... -void test_rgb() { - unittest_assertequals(colour_rgb(100, 40, 0), '#ff6600', 'from rgb'); -} - -String colour_random() { - String hex = '0123456789abcdef'; - var rnd = new Math.Random(); - return '#${hex[rnd.nextInt(16)]}${hex[rnd.nextInt(16)]}' - '${hex[rnd.nextInt(16)]}${hex[rnd.nextInt(16)]}' - '${hex[rnd.nextInt(16)]}${hex[rnd.nextInt(16)]}'; -} - -/// Describe this function... -void test_colour_random() { - for (int count4 = 0; count4 < 100; count4++) { - item = colour_random(); - unittest_assertequals(item.length, 7, ['length of random colour string: ',item].join()); - unittest_assertequals(item[0], '#', ['format of random colour string: ',item].join()); - for (i = 1; i <= 6; i++) { - unittest_assertequals(0 != 'abcdefABDEF0123456789'.indexOf(item[((i + 1) - 1)]) + 1, true, ['contents of random colour string: ',item,' at index: ',i + 1].join()); - } - } -} - -String colour_blend(String c1, String c2, num ratio) { - ratio = Math.max(Math.min(ratio, 1), 0); - int r1 = int.parse('0x${c1.substring(1, 3)}'); - int g1 = int.parse('0x${c1.substring(3, 5)}'); - int b1 = int.parse('0x${c1.substring(5, 7)}'); - int r2 = int.parse('0x${c2.substring(1, 3)}'); - int g2 = int.parse('0x${c2.substring(3, 5)}'); - int b2 = int.parse('0x${c2.substring(5, 7)}'); - num rn = (r1 * (1 - ratio) + r2 * ratio).round(); - String rs = rn.toInt().toRadixString(16); - num gn = (g1 * (1 - ratio) + g2 * ratio).round(); - String gs = gn.toInt().toRadixString(16); - num bn = (b1 * (1 - ratio) + b2 * ratio).round(); - String bs = bn.toInt().toRadixString(16); - rs = '0$rs'; - rs = rs.substring(rs.length - 2); - gs = '0$gs'; - gs = gs.substring(gs.length - 2); - bs = '0$bs'; - bs = bs.substring(bs.length - 2); - return '#$rs$gs$bs'; -} - -/// Describe this function... -void test_blend() { - unittest_assertequals(colour_blend('#ff0000', colour_rgb(100, 40, 0), 0.4), '#ff2900', 'blend'); -} - /// Describe this function... void test_procedure() { procedure_1(8, 2); @@ -1642,7 +1554,6 @@ main() { test_count_text(); test_text_reverse(); test_replace(); - test_multiline(); print(unittest_report()); unittestResults = null; @@ -1671,15 +1582,6 @@ main() { print(unittest_report()); unittestResults = null; - unittestResults = []; - print('\n====================\n\nRunning suite: Colour'); - test_colour_picker(); - test_blend(); - test_rgb(); - test_colour_random(); - print(unittest_report()); - unittestResults = null; - unittestResults = []; print('\n====================\n\nRunning suite: Variables'); item = 123; diff --git a/tests/generators/golden/generated.js b/tests/generators/golden/generated.js index ec103f0ba..9fa4f1d93 100644 --- a/tests/generators/golden/generated.js +++ b/tests/generators/golden/generated.js @@ -1,6 +1,6 @@ 'use strict'; -var unittestResults, test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy; +var unittestResults, test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy; function unittest_report() { // Create test report. @@ -920,20 +920,6 @@ function test_replace() { assertEquals(textReplace('', 'a', 'chicken'), '', 'empty source'); } -// Tests the "multiline" block. -function test_multiline() { - assertEquals('', '', 'no text'); - assertEquals('Google', 'Google', 'simple'); - assertEquals('paragraph' + '\n' + - 'with newlines' + '\n' + - 'yup', 'paragraph' + '\n' + - 'with newlines' + '\n' + - 'yup', 'no compile error with newlines'); - assertEquals(textCount('bark bark' + '\n' + - 'bark bark bark' + '\n' + - 'bark bark bark bark', 'bark'), 9, 'count with newlines'); -} - // Checks that the number of calls is one in order // to confirm that a function was only called once. function check_number_of_calls2(test_name) { @@ -1376,65 +1362,6 @@ function test_lists_reverse() { assertEquals(list.slice().reverse(), [], 'empty list'); } -// Describe this function... -function test_colour_picker() { - assertEquals('#ff6600', '#ff6600', 'static colour'); -} - -function colourRgb(r, g, b) { - r = Math.max(Math.min(Number(r), 100), 0) * 2.55; - g = Math.max(Math.min(Number(g), 100), 0) * 2.55; - b = Math.max(Math.min(Number(b), 100), 0) * 2.55; - r = ('0' + (Math.round(r) || 0).toString(16)).slice(-2); - g = ('0' + (Math.round(g) || 0).toString(16)).slice(-2); - b = ('0' + (Math.round(b) || 0).toString(16)).slice(-2); - return '#' + r + g + b; -} - -// Describe this function... -function test_rgb() { - assertEquals(colourRgb(100, 40, 0), '#ff6600', 'from rgb'); -} - -function colourRandom() { - var num = Math.floor(Math.random() * Math.pow(2, 24)); - return '#' + ('00000' + num.toString(16)).substr(-6); -} - -// Describe this function... -function test_colour_random() { - for (var count4 = 0; count4 < 100; count4++) { - item = colourRandom(); - assertEquals(item.length, 7, 'length of random colour string: ' + String(item)); - assertEquals(item.charAt(0), '#', 'format of random colour string: ' + String(item)); - for (i = 1; i <= 6; i++) { - assertEquals(0 != 'abcdefABDEF0123456789'.indexOf(item.charAt(((i + 1) - 1))) + 1, true, ['contents of random colour string: ',item,' at index: ',i + 1].join('')); - } - } -} - -function colourBlend(c1, c2, ratio) { - ratio = Math.max(Math.min(Number(ratio), 1), 0); - var r1 = parseInt(c1.substring(1, 3), 16); - var g1 = parseInt(c1.substring(3, 5), 16); - var b1 = parseInt(c1.substring(5, 7), 16); - var r2 = parseInt(c2.substring(1, 3), 16); - var g2 = parseInt(c2.substring(3, 5), 16); - var b2 = parseInt(c2.substring(5, 7), 16); - var r = Math.round(r1 * (1 - ratio) + r2 * ratio); - var g = Math.round(g1 * (1 - ratio) + g2 * ratio); - var b = Math.round(b1 * (1 - ratio) + b2 * ratio); - r = ('0' + (r || 0).toString(16)).slice(-2); - g = ('0' + (g || 0).toString(16)).slice(-2); - b = ('0' + (b || 0).toString(16)).slice(-2); - return '#' + r + g + b; -} - -// Describe this function... -function test_blend() { - assertEquals(colourBlend('#ff0000', colourRgb(100, 40, 0), 0.4), '#ff2900', 'blend'); -} - // Describe this function... function test_procedure() { procedure_1(8, 2); @@ -1576,7 +1503,6 @@ test_trim(); test_count_text(); test_text_reverse(); test_replace(); -test_multiline(); console.log(unittest_report()); unittestResults = null; @@ -1605,15 +1531,6 @@ test_lists_reverse(); console.log(unittest_report()); unittestResults = null; -unittestResults = []; -console.log('\n====================\n\nRunning suite: Colour') -test_colour_picker(); -test_blend(); -test_rgb(); -test_colour_random(); -console.log(unittest_report()); -unittestResults = null; - unittestResults = []; console.log('\n====================\n\nRunning suite: Variables') item = 123; diff --git a/tests/generators/golden/generated.lua b/tests/generators/golden/generated.lua index 085a76070..63dbe0af4 100644 --- a/tests/generators/golden/generated.lua +++ b/tests/generators/golden/generated.lua @@ -1015,21 +1015,6 @@ function test_replace() end --- Tests the "multiline" block. -function test_multiline() - assertEquals('', '', 'no text') - assertEquals('Google', 'Google', 'simple') - assertEquals('paragraph' .. '\n' .. - 'with newlines' .. '\n' .. - 'yup', 'paragraph' .. '\n' .. - 'with newlines' .. '\n' .. - 'yup', 'no compile error with newlines') - assertEquals(text_count('bark bark' .. '\n' .. - 'bark bark bark' .. '\n' .. - 'bark bark bark bark', 'bark'), 9, 'count with newlines') -end - - -- Checks that the number of calls is one in order -- to confirm that a function was only called once. function check_number_of_calls2(test_name) @@ -1645,58 +1630,6 @@ function test_lists_reverse() end --- Describe this function... -function test_colour_picker() - assertEquals('#ff6600', '#ff6600', 'static colour') -end - - -function colour_rgb(r, g, b) - r = math.floor(math.min(100, math.max(0, r)) * 2.55 + .5) - g = math.floor(math.min(100, math.max(0, g)) * 2.55 + .5) - b = math.floor(math.min(100, math.max(0, b)) * 2.55 + .5) - return string.format("#%02x%02x%02x", r, g, b) -end - --- Describe this function... -function test_rgb() - assertEquals(colour_rgb(100, 40, 0), '#ff6600', 'from rgb') -end - - --- Describe this function... -function test_colour_random() - for count4 = 1, 100 do - item = string.format("#%06x", math.random(0, 2^24 - 1)) - assertEquals(#item, 7, 'length of random colour string: ' .. item) - assertEquals(string.sub(item, 1, 1), '#', 'format of random colour string: ' .. item) - for i = 1, 6, 1 do - assertEquals(0 ~= firstIndexOf('abcdefABDEF0123456789', text_char_at(item, i + 1)), true, table.concat({'contents of random colour string: ', item, ' at index: ', i + 1})) - end - end -end - - -function colour_blend(colour1, colour2, ratio) - local r1 = tonumber(string.sub(colour1, 2, 3), 16) - local r2 = tonumber(string.sub(colour2, 2, 3), 16) - local g1 = tonumber(string.sub(colour1, 4, 5), 16) - local g2 = tonumber(string.sub(colour2, 4, 5), 16) - local b1 = tonumber(string.sub(colour1, 6, 7), 16) - local b2 = tonumber(string.sub(colour2, 6, 7), 16) - local ratio = math.min(1, math.max(0, ratio)) - local r = math.floor(r1 * (1 - ratio) + r2 * ratio + .5) - local g = math.floor(g1 * (1 - ratio) + g2 * ratio + .5) - local b = math.floor(b1 * (1 - ratio) + b2 * ratio + .5) - return string.format("#%02x%02x%02x", r, g, b) -end - --- Describe this function... -function test_blend() - assertEquals(colour_blend('#ff0000', colour_rgb(100, 40, 0), 0.4), '#ff2900', 'blend') -end - - -- Describe this function... function test_procedure() procedure_1(8, 2) @@ -1846,7 +1779,6 @@ test_trim() test_count_text() test_text_reverse() test_replace() -test_multiline() print(unittest_report()) unittestResults = nil @@ -1875,15 +1807,6 @@ test_lists_reverse() print(unittest_report()) unittestResults = nil -unittestResults = {} -print('\n====================\n\nRunning suite: Colour') -test_colour_picker() -test_blend() -test_rgb() -test_colour_random() -print(unittest_report()) -unittestResults = nil - unittestResults = {} print('\n====================\n\nRunning suite: Variables') item = 123 diff --git a/tests/generators/golden/generated.php b/tests/generators/golden/generated.php index 4a16b6f5c..d497f1a70 100644 --- a/tests/generators/golden/generated.php +++ b/tests/generators/golden/generated.php @@ -53,7 +53,7 @@ function unittest_fail($message) { // Describe this function... function test_if() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; if (false) { unittest_fail('if false'); } @@ -91,7 +91,7 @@ function test_if() { // Describe this function... function test_ifelse() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $ok = false; if (true) { $ok = true; @@ -110,7 +110,7 @@ function test_ifelse() { // Describe this function... function test_equalities() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(2 == 2, true, 'Equal yes'); assertEquals(3 == 4, false, 'Equal no'); assertEquals(5 != 6, true, 'Not equal yes'); @@ -127,7 +127,7 @@ function test_equalities() { // Describe this function... function test_and() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(true && true, true, 'And true/true'); assertEquals(false && true, false, 'And false/true'); assertEquals(true && false, false, 'And true/false'); @@ -136,7 +136,7 @@ function test_and() { // Describe this function... function test_or() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(true || true, true, 'Or true/true'); assertEquals(false || true, true, 'Or false/true'); assertEquals(true || false, true, 'Or true/false'); @@ -145,14 +145,14 @@ function test_or() { // Describe this function... function test_ternary() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(true ? 42 : 99, 42, 'if true'); assertEquals(false ? 42 : 99, 99, 'if true'); } // Describe this function... function test_foreach() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $log = ''; foreach (array('a', 'b', 'c') as $x) { $log .= $x; @@ -162,7 +162,7 @@ function test_foreach() { // Describe this function... function test_repeat() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $count = 0; for ($count2 = 0; $count2 < 10; $count2++) { $count += 1; @@ -172,7 +172,7 @@ function test_repeat() { // Describe this function... function test_while() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; while (false) { unittest_fail('while 0'); } @@ -193,7 +193,7 @@ function test_while() { // Describe this function... function test_repeat_ext() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $count = 0; for ($count3 = 0; $count3 < 10; $count3++) { $count += 1; @@ -203,7 +203,7 @@ function test_repeat_ext() { // Describe this function... function test_count_by() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $log = ''; for ($x = 1; $x <= 8; $x += 2) { $log .= $x; @@ -256,7 +256,7 @@ function test_count_by() { // Describe this function... function test_count_loops() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $log = ''; for ($x = 1; $x <= 8; $x++) { $log .= $x; @@ -293,7 +293,7 @@ function test_count_loops() { // Describe this function... function test_continue() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $log = ''; $count = 0; while ($count != 8) { @@ -334,7 +334,7 @@ function test_continue() { // Describe this function... function test_break() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $count = 1; while ($count != 10) { if ($count == 5) { @@ -371,7 +371,7 @@ function test_break() { // Tests the "single" block. function test_single() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(sqrt(25), 5, 'sqrt'); assertEquals(abs(-25), 25, 'abs'); assertEquals(-(-25), 25, 'negate'); @@ -384,7 +384,7 @@ function test_single() { // Tests the "arithmetic" block for all operations and checks // parenthesis are properly generated for different orders. function test_arithmetic() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(1 + 2, 3, 'add'); assertEquals(1 - 2, -1, 'subtract'); assertEquals(1 - (0 + 2), -1, 'subtract order with add'); @@ -399,7 +399,7 @@ function test_arithmetic() { // Tests the "trig" block. function test_trig() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(sin(90 / 180 * pi()), 1, 'sin'); assertEquals(cos(180 / 180 * pi()), -1, 'cos'); assertEquals(tan(0 / 180 * pi()), 0, 'tan'); @@ -410,7 +410,7 @@ function test_trig() { // Tests the "constant" blocks. function test_constant() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(floor(M_PI * 1000), 3141, 'const pi'); assertEquals(floor(M_E * 1000), 2718, 'const e'); assertEquals(floor(((1 + sqrt(5)) / 2) * 1000), 1618, 'const golden'); @@ -440,7 +440,7 @@ function math_isPrime($n) { // Tests the "number property" blocks. function test_number_properties() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(42 % 2 == 0, true, 'even'); assertEquals(42.1 % 2 == 1, false, 'odd'); assertEquals(math_isPrime(5), true, 'prime 5'); @@ -458,7 +458,7 @@ function test_number_properties() { // Tests the "round" block. function test_round() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(round(42.42), 42, 'round'); assertEquals(ceil(-42.42), -42, 'round up'); assertEquals(floor(42.42), 42, 'round down'); @@ -466,7 +466,7 @@ function test_round() { // Tests the "change" block. function test_change() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $varToChange = 100; $varToChange += 42; assertEquals($varToChange, 142, 'change'); @@ -512,7 +512,7 @@ function indexOf($haystack, $needle) { // Tests the "list operation" blocks. function test_operations_on_list() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(array_sum((array(3, 4, 5))), 12, 'sum'); assertEquals(min((array(3, 4, 5))), 3, 'min'); assertEquals(max((array(3, 4, 5))), 5, 'max'); @@ -526,13 +526,13 @@ function test_operations_on_list() { // Tests the "mod" block. function test_mod() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(42 % 5, 2, 'mod'); } // Tests the "constrain" block. function test_constraint() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(min(max(100, 0), 42), 42, 'constraint'); } @@ -545,7 +545,7 @@ function math_random_int($a, $b) { // Tests the "random integer" block. function test_random_integer() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $rand = math_random_int(5, 10); assertEquals($rand >= 5 && $rand <= 10, true, 'randRange'); assertEquals(is_int($rand), true, 'randInteger'); @@ -553,14 +553,14 @@ function test_random_integer() { // Tests the "random fraction" block. function test_random_fraction() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $rand = (float)rand()/(float)getrandmax(); assertEquals($rand >= 0 && $rand <= 1, true, 'randFloat'); } // Describe this function... function test_atan2() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(atan2(5, -5) / pi() * 180, 135, 'atan2'); assertEquals(atan2(-12, 0) / pi() * 180, -90, 'atan2'); } @@ -568,14 +568,14 @@ function test_atan2() { // Checks that the number of calls is one in order // to confirm that a function was only called once. function check_number_of_calls($test_name) { - global $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $test_name .= 'number of calls'; assertEquals($number_of_calls, 1, $test_name); } // Tests the "create text with" block with varying number of inputs. function test_create_text() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals('', '', 'no text'); assertEquals('Hello', 'Hello', 'create single'); assertEquals(-1, '-1', 'create single number'); @@ -587,13 +587,13 @@ function test_create_text() { // Creates an empty string for use with the empty test. function get_empty() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; return ''; } // Tests the "is empty" block". function test_empty_text() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(empty('Google'), false, 'not empty'); assertEquals(empty(''), true, 'empty'); assertEquals(empty(get_empty()), true, 'empty complex'); @@ -609,7 +609,7 @@ function length($value) { // Tests the "length" block. function test_text_length() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(length(''), 0, 'zero length'); assertEquals(length('Google'), 6, 'non-zero length'); assertEquals(length(true ? 'car' : null), 3, 'length order'); @@ -617,7 +617,7 @@ function test_text_length() { // Tests the "append text" block with different types of parameters. function test_append() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $item = 'Miserable'; $item .= 'Failure'; assertEquals($item, 'MiserableFailure', 'append text'); @@ -641,7 +641,7 @@ function text_lastIndexOf($text, $search) { // Tests the "find" block with a variable. function test_find_text_simple() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $text = 'Banana'; assertEquals(text_indexOf($text, 'an'), 2, 'find first simple'); assertEquals(text_lastIndexOf($text, 'an'), 4, 'find last simple'); @@ -650,14 +650,14 @@ function test_find_text_simple() { // Creates a string for use with the find test. function get_fruit() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $number_of_calls += 1; return 'Banana'; } // Tests the "find" block with a function call. function test_find_text_complex() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $number_of_calls = 0; assertEquals(text_indexOf(get_fruit(), 'an'), 2, 'find first complex'); check_number_of_calls('find first complex'); @@ -684,7 +684,7 @@ function text_random_letter($text) { // Tests the "get letter" block with a variable. function test_get_text_simple() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $text = 'Blockly'; assertEquals(substr($text, 0, 1), 'B', 'get first simple'); assertEquals(substr($text, -1), 'y', 'get last simple'); @@ -698,14 +698,14 @@ function test_get_text_simple() { // Creates a string for use with the get test. function get_Blockly() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $number_of_calls += 1; return 'Blockly'; } // Tests the "get letter" block with a function call. function test_get_text_complex() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $text = 'Blockly'; $number_of_calls = 0; assertEquals(substr(get_Blockly(), 0, 1), 'B', 'get first complex'); @@ -742,7 +742,7 @@ function test_get_text_complex() { // Creates a string for use with the substring test. function get_numbers() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $number_of_calls += 1; return '123456789'; } @@ -770,7 +770,7 @@ function text_get_substring($text, $where1, $at1, $where2, $at2) { // Tests the "get substring" block with a variable. function test_substring_simple() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $text = '123456789'; assertEquals(text_get_substring($text, 'FROM_START', 1, 'FROM_START', 2), '23', 'substring # simple'); assertEquals(text_get_substring($text, 'FROM_START', ((true ? 2 : null) - 1), 'FROM_START', ((true ? 3 : null) - 1)), '23', 'substring # simple order'); @@ -792,7 +792,7 @@ function test_substring_simple() { // Tests the "get substring" block with a function call. function test_substring_complex() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $number_of_calls = 0; assertEquals(text_get_substring(get_numbers(), 'FROM_START', 1, 'FROM_START', 2), '23', 'substring # complex'); check_number_of_calls('substring # complex'); @@ -841,7 +841,7 @@ function test_substring_complex() { // Tests the "change casing" block. function test_case() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $text = 'Hello World'; assertEquals(strtoupper($text), 'HELLO WORLD', 'uppercase'); assertEquals(strtoupper(true ? $text : null), 'HELLO WORLD', 'uppercase order'); @@ -855,7 +855,7 @@ function test_case() { // Tests the "trim" block. function test_trim() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $text = ' abc def '; assertEquals(trim($text), 'abc def', 'trim both'); assertEquals(trim(true ? $text : null), 'abc def', 'trim both order'); @@ -867,7 +867,7 @@ function test_trim() { // Tests the "trim" block. function test_count_text() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $text = 'woolloomooloo'; assertEquals(strlen('o') === 0 ? strlen($text) + 1 : substr_count($text, 'o'), 8, 'len 1'); assertEquals(strlen('oo') === 0 ? strlen($text) + 1 : substr_count($text, 'oo'), 4, 'len 2'); @@ -880,7 +880,7 @@ function test_count_text() { // Tests the "trim" block. function test_text_reverse() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(strrev(''), '', 'empty string'); assertEquals(strrev('a'), 'a', 'len 1'); assertEquals(strrev('ab'), 'ba', 'len 2'); @@ -889,7 +889,7 @@ function test_text_reverse() { // Tests the "trim" block. function test_replace() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(str_replace('oo', '123', 'woolloomooloo'), 'w123ll123m123l123', 'replace all instances 1'); assertEquals(str_replace('.oo', 'X', 'woolloomooloo'), 'woolloomooloo', 'literal string replacement'); assertEquals(str_replace('abc', 'X', 'woolloomooloo'), 'woolloomooloo', 'not found'); @@ -899,27 +899,10 @@ function test_replace() { assertEquals(str_replace('a', 'chicken', ''), '', 'empty source'); } -// Tests the "multiline" block. -function test_multiline() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; - assertEquals('', '', 'no text'); - assertEquals('Google', 'Google', 'simple'); - assertEquals('paragraph' . "\n" . - 'with newlines' . "\n" . - 'yup', 'paragraph' . "\n" . - 'with newlines' . "\n" . - 'yup', 'no compile error with newlines'); - assertEquals(strlen('bark') === 0 ? strlen('bark bark' . "\n" . - 'bark bark bark' . "\n" . - 'bark bark bark bark') + 1 : substr_count('bark bark' . "\n" . - 'bark bark bark' . "\n" . - 'bark bark bark bark', 'bark'), 9, 'count with newlines'); -} - // Checks that the number of calls is one in order // to confirm that a function was only called once. function check_number_of_calls2($test_name) { - global $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $test_name .= 'number of calls'; assertEquals($number_of_calls, 1, $test_name); } @@ -934,7 +917,7 @@ function lists_repeat($value, $count) { // Tests the "create list with" and "create empty list" blocks. function test_create_lists() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(array(), array(), 'create empty'); assertEquals(array(true, 'love'), array(true, 'love'), 'create items'); assertEquals(lists_repeat('Eject', 3), array('Eject', 'Eject', 'Eject'), 'create repeated'); @@ -943,13 +926,13 @@ function test_create_lists() { // Creates an empty list for use with the empty test. function get_empty_list() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; return array(); } // Tests the "is empty" block. function test_lists_empty() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(empty((array(0))), false, 'not empty'); assertEquals(empty((array())), true, 'empty'); assertEquals(empty((get_empty_list())), true, 'empty complex'); @@ -958,7 +941,7 @@ function test_lists_empty() { // Tests the "length" block. function test_lists_length() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(length(array()), 0, 'zero length'); assertEquals(length(array('cat')), 1, 'one length'); assertEquals(length(array('cat', true, array())), 3, 'three length'); @@ -975,7 +958,7 @@ function lastIndexOf($haystack, $needle) { // Tests the "find" block with a variable. function test_find_lists_simple() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array('Alice', 'Eve', 'Bob', 'Eve'); assertEquals(indexOf($list2, 'Eve'), 2, 'find first simple'); assertEquals(lastIndexOf($list2, 'Eve'), 4, 'find last simple'); @@ -984,14 +967,14 @@ function test_find_lists_simple() { // Creates a list for use with the find test. function get_names() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $number_of_calls += 1; return array('Alice', 'Eve', 'Bob', 'Eve'); } // Tests the "find" block with a function call. function test_find_lists_complex() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $number_of_calls = 0; assertEquals(indexOf(get_names(), 'Eve'), 2, 'find first complex'); check_number_of_calls('find first complex'); @@ -1018,7 +1001,7 @@ function lists_get_random_item($list) { // Tests the "get" block with a variable. function test_get_lists_simple() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array('Kirk', 'Spock', 'McCoy'); assertEquals($list2[0], 'Kirk', 'get first simple'); assertEquals(end($list2), 'McCoy', 'get last simple'); @@ -1032,7 +1015,7 @@ function test_get_lists_simple() { // Tests the "get" block with create list call. function test_get_lists_create_list() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(array('Kirk', 'Spock', 'McCoy')[0], 'Kirk', 'get first create list'); assertEquals(end(array('Kirk', 'Spock', 'McCoy')), 'McCoy', 'get last simple'); assertEquals(indexOf(array('Kirk', 'Spock', 'McCoy'), lists_get_random_item(array('Kirk', 'Spock', 'McCoy'))) > 0, true, 'get random simple'); @@ -1045,14 +1028,14 @@ function test_get_lists_create_list() { // Creates a list for use with the get test. function get_star_wars() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $number_of_calls += 1; return array('Kirk', 'Spock', 'McCoy'); } // Tests the "get" block with a function call. function test_get_lists_complex() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array('Kirk', 'Spock', 'McCoy'); $number_of_calls = 0; assertEquals(get_star_wars()[0], 'Kirk', 'get first complex'); @@ -1095,7 +1078,7 @@ function lists_get_remove_random_item(&$list) { // Tests the "get and remove" block. function test_getRemove() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array('Kirk', 'Spock', 'McCoy'); assertEquals(array_shift($list2), 'Kirk', 'getremove first'); assertEquals($list2, array('Spock', 'McCoy'), 'getremove first list'); @@ -1135,7 +1118,7 @@ function lists_remove_random_item(&$list) { // Tests the "remove" block. function test_remove() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array('Kirk', 'Spock', 'McCoy'); array_shift($list2); assertEquals($list2, array('Spock', 'McCoy'), 'remove first list'); @@ -1180,7 +1163,7 @@ function lists_set_from_end(&$list, $at, $value) { // Tests the "set" block. function test_set() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array('Picard', 'Riker', 'Crusher'); $list2[0] = 'Jean-Luc'; assertEquals($list2, array('Jean-Luc', 'Riker', 'Crusher'), 'set first list'); @@ -1224,7 +1207,7 @@ function lists_insert_from_end(&$list, $at, $value) { // Tests the "insert" block. function test_insert() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array('Picard', 'Riker', 'Crusher'); array_unshift($list2, 'Data'); assertEquals($list2, array('Data', 'Picard', 'Riker', 'Crusher'), 'insert first list'); @@ -1264,7 +1247,7 @@ function test_insert() { // Tests the "get sub-list" block with a variable. function test_sublist_simple() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array('Columbia', 'Challenger', 'Discovery', 'Atlantis', 'Endeavour'); assertEquals(array_slice($list2, 1, 2 - 1 + 1), array('Challenger', 'Discovery'), 'sublist # simple'); assertEquals(array_slice($list2, ((true ? 2 : null) - 1), ((true ? 3 : null) - 1) - ((true ? 2 : null) - 1) + 1), array('Challenger', 'Discovery'), 'sublist # simple order'); @@ -1290,7 +1273,7 @@ function test_sublist_simple() { // Creates a list for use with the sublist test. function get_space_shuttles() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $number_of_calls += 1; return array('Columbia', 'Challenger', 'Discovery', 'Atlantis', 'Endeavour'); } @@ -1318,7 +1301,7 @@ function lists_get_sublist($list, $where1, $at1, $where2, $at2) { // Tests the "get sub-list" block with a function call. function test_sublist_complex() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $number_of_calls = 0; assertEquals(array_slice(get_space_shuttles(), 1, 2 - 1 + 1), array('Challenger', 'Discovery'), 'sublist # start complex'); check_number_of_calls('sublist # start complex'); @@ -1367,7 +1350,7 @@ function test_sublist_complex() { // Tests the "join" block. function test_join() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array('Vulcan', 'Klingon', 'Borg'); assertEquals(implode(',', $list2), 'Vulcan,Klingon,Borg', 'join'); assertEquals(implode(',', true ? $list2 : null), 'Vulcan,Klingon,Borg', 'join order'); @@ -1375,7 +1358,7 @@ function test_join() { // Tests the "split" block. function test_split() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $text = 'Vulcan,Klingon,Borg'; assertEquals(explode(',', $text), array('Vulcan', 'Klingon', 'Borg'), 'split'); assertEquals(explode(',', true ? $text : null), array('Vulcan', 'Klingon', 'Borg'), 'split order'); @@ -1398,7 +1381,7 @@ function lists_sort($list, $type, $direction) { // Tests the "alphabetic sort" block. function test_sort_alphabetic() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array('Vulcan', 'klingon', 'Borg'); assertEquals(lists_sort($list2, "TEXT", 1), array('Borg', 'Vulcan', 'klingon'), 'sort alphabetic ascending'); assertEquals(lists_sort(true ? $list2 : null, "TEXT", 1), array('Borg', 'Vulcan', 'klingon'), 'sort alphabetic ascending order'); @@ -1406,7 +1389,7 @@ function test_sort_alphabetic() { // Tests the "alphabetic sort ignore case" block. function test_sort_ignoreCase() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array('Vulcan', 'klingon', 'Borg'); assertEquals(lists_sort($list2, "IGNORE_CASE", 1), array('Borg', 'klingon', 'Vulcan'), 'sort ignore case ascending'); assertEquals(lists_sort(true ? $list2 : null, "IGNORE_CASE", 1), array('Borg', 'klingon', 'Vulcan'), 'sort ignore case ascending order'); @@ -1414,7 +1397,7 @@ function test_sort_ignoreCase() { // Tests the "numeric sort" block. function test_sort_numeric() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array(8, 18, -1); assertEquals(lists_sort($list2, "NUMERIC", -1), array(18, 8, -1), 'sort numeric descending'); assertEquals(lists_sort(true ? $list2 : null, "NUMERIC", -1), array(18, 8, -1), 'sort numeric descending order'); @@ -1422,7 +1405,7 @@ function test_sort_numeric() { // Tests the "list reverse" block. function test_lists_reverse() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $list2 = array(8, 18, -1, 64); assertEquals(array_reverse($list2), array(64, -1, 18, 8), 'reverse a copy'); assertEquals($list2, array(8, 18, -1, 64), 'reverse a copy original'); @@ -1430,73 +1413,9 @@ function test_lists_reverse() { assertEquals(array_reverse($list2), array(), 'empty list'); } -// Describe this function... -function test_colour_picker() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; - assertEquals('#ff6600', '#ff6600', 'static colour'); -} - -function colour_rgb($r, $g, $b) { - $r = round(max(min($r, 100), 0) * 2.55); - $g = round(max(min($g, 100), 0) * 2.55); - $b = round(max(min($b, 100), 0) * 2.55); - $hex = '#'; - $hex .= str_pad(dechex($r), 2, '0', STR_PAD_LEFT); - $hex .= str_pad(dechex($g), 2, '0', STR_PAD_LEFT); - $hex .= str_pad(dechex($b), 2, '0', STR_PAD_LEFT); - return $hex; -} - -// Describe this function... -function test_rgb() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; - assertEquals(colour_rgb(100, 40, 0), '#ff6600', 'from rgb'); -} - -function colour_random() { - return '#' . str_pad(dechex(mt_rand(0, 0xFFFFFF)), 6, '0', STR_PAD_LEFT); -} - -// Describe this function... -function test_colour_random() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; - for ($count4 = 0; $count4 < 100; $count4++) { - $item = colour_random(); - assertEquals(length($item), 7, 'length of random colour string: ' . $item); - assertEquals(substr($item, 0, 1), '#', 'format of random colour string: ' . $item); - for ($i = 1; $i <= 6; $i++) { - assertEquals(0 != text_indexOf('abcdefABDEF0123456789', substr($item, (($i + 1) - 1), 1)), true, implode('', array('contents of random colour string: ',$item,' at index: ',$i + 1))); - } - } -} - -function colour_blend($c1, $c2, $ratio) { - $ratio = max(min($ratio, 1), 0); - $r1 = hexdec(substr($c1, 1, 2)); - $g1 = hexdec(substr($c1, 3, 2)); - $b1 = hexdec(substr($c1, 5, 2)); - $r2 = hexdec(substr($c2, 1, 2)); - $g2 = hexdec(substr($c2, 3, 2)); - $b2 = hexdec(substr($c2, 5, 2)); - $r = round($r1 * (1 - $ratio) + $r2 * $ratio); - $g = round($g1 * (1 - $ratio) + $g2 * $ratio); - $b = round($b1 * (1 - $ratio) + $b2 * $ratio); - $hex = '#'; - $hex .= str_pad(dechex($r), 2, '0', STR_PAD_LEFT); - $hex .= str_pad(dechex($g), 2, '0', STR_PAD_LEFT); - $hex .= str_pad(dechex($b), 2, '0', STR_PAD_LEFT); - return $hex; -} - -// Describe this function... -function test_blend() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; - assertEquals(colour_blend('#ff0000', colour_rgb(100, 40, 0), 0.4), '#ff2900', 'blend'); -} - // Describe this function... function test_procedure() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; procedure_1(8, 2); assertEquals($proc_z, 4, 'procedure with global'); $proc_w = false; @@ -1509,13 +1428,13 @@ function test_procedure() { // Describe this function... function procedure_1($proc_x, $proc_y) { - global $test_name, $naked, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $proc_z = $proc_x / $proc_y; } // Describe this function... function procedure_2($proc_x) { - global $test_name, $naked, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; if ($proc_x) { return; } @@ -1524,7 +1443,7 @@ function procedure_2($proc_x) { // Describe this function... function test_function() { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; assertEquals(function_1(2, 3), -1, 'function with arguments'); assertEquals($func_z, 'side effect', 'function with side effect'); $func_a = 'unchanged'; @@ -1537,21 +1456,21 @@ function test_function() { // Describe this function... function function_1($func_x, $func_y) { - global $test_name, $naked, $proc_x, $proc_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_a, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $func_z = 'side effect'; return $func_x - $func_y; } // Describe this function... function function_2($func_a) { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; $func_a += 1; return $func_a . $func_c; } // Describe this function... function function_3($func_a) { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $n, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; if ($func_a) { return true; } @@ -1560,7 +1479,7 @@ function function_3($func_a) { // Describe this function... function recurse($n) { - global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $i, $loglist, $changing_list, $list_copy, $unittestResults; + global $test_name, $naked, $proc_x, $proc_y, $func_x, $func_y, $func_a, $ok, $log, $count, $varToChange, $rand, $item, $text, $number_of_calls, $list2, $proc_z, $func_z, $x, $proc_w, $func_c, $if2, $loglist, $changing_list, $list_copy, $unittestResults; if ($n > 0) { $text = implode('', array(recurse($n - 1),$n,recurse($n - 1))); } else { @@ -1643,7 +1562,6 @@ test_trim(); test_count_text(); test_text_reverse(); test_replace(); -test_multiline(); print(unittest_report()); $unittestResults = null; @@ -1672,15 +1590,6 @@ test_lists_reverse(); print(unittest_report()); $unittestResults = null; -$unittestResults = array(); -print("\n====================\n\nRunning suite: Colour\n"); -test_colour_picker(); -test_blend(); -test_rgb(); -test_colour_random(); -print(unittest_report()); -$unittestResults = null; - $unittestResults = array(); print("\n====================\n\nRunning suite: Variables\n"); $item = 123; diff --git a/tests/generators/golden/generated.py b/tests/generators/golden/generated.py index e4375dbbc..bafa0983b 100644 --- a/tests/generators/golden/generated.py +++ b/tests/generators/golden/generated.py @@ -27,7 +27,6 @@ x = None proc_w = None func_c = None if2 = None -i = None loglist = None changing_list = None list_copy = None @@ -73,7 +72,7 @@ def fail(message): # Describe this function... def test_if(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults if False: fail('if false') ok = False @@ -105,7 +104,7 @@ def test_if(): # Describe this function... def test_ifelse(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults ok = False if True: ok = True @@ -121,7 +120,7 @@ def test_ifelse(): # Describe this function... def test_equalities(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(2 == 2, True, 'Equal yes') assertEquals(3 == 4, False, 'Equal no') assertEquals(5 != 6, True, 'Not equal yes') @@ -137,7 +136,7 @@ def test_equalities(): # Describe this function... def test_and(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(True and True, True, 'And true/true') assertEquals(False and True, False, 'And false/true') assertEquals(True and False, False, 'And true/false') @@ -145,7 +144,7 @@ def test_and(): # Describe this function... def test_or(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(True or True, True, 'Or true/true') assertEquals(False or True, True, 'Or false/true') assertEquals(True or False, True, 'Or true/false') @@ -153,13 +152,13 @@ def test_or(): # Describe this function... def test_ternary(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(42 if True else 99, 42, 'if true') assertEquals(42 if False else 99, 99, 'if true') # Describe this function... def test_foreach(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults log = '' for x in ['a', 'b', 'c']: log = str(log) + str(x) @@ -167,7 +166,7 @@ def test_foreach(): # Describe this function... def test_repeat(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults count = 0 for count2 in range(10): count = (count if isinstance(count, Number) else 0) + 1 @@ -175,7 +174,7 @@ def test_repeat(): # Describe this function... def test_while(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults while False: fail('while 0') while not True: @@ -191,7 +190,7 @@ def test_while(): # Describe this function... def test_repeat_ext(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults count = 0 for count3 in range(10): count = (count if isinstance(count, Number) else 0) + 1 @@ -209,7 +208,7 @@ def downRange(start, stop, step): # Describe this function... def test_count_by(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults log = '' for x in range(1, 9, 2): log = str(log) + str(x) @@ -245,7 +244,7 @@ def test_count_by(): # Describe this function... def test_count_loops(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults log = '' for x in range(1, 9): log = str(log) + str(x) @@ -269,7 +268,7 @@ def test_count_loops(): # Describe this function... def test_continue(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults log = '' count = 0 while count != 8: @@ -301,7 +300,7 @@ def test_continue(): # Describe this function... def test_break(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults count = 1 while count != 10: if count == 5: @@ -329,7 +328,7 @@ def test_break(): # Tests the "single" block. def test_single(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(math.sqrt(25), 5, 'sqrt') assertEquals(math.fabs(-25), 25, 'abs') assertEquals(-(-25), 25, 'negate') @@ -341,7 +340,7 @@ def test_single(): # Tests the "arithmetic" block for all operations and checks # parenthesis are properly generated for different orders. def test_arithmetic(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(1 + 2, 3, 'add') assertEquals(1 - 2, -1, 'subtract') assertEquals(1 - (0 + 2), -1, 'subtract order with add') @@ -355,7 +354,7 @@ def test_arithmetic(): # Tests the "trig" block. def test_trig(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(math.sin(90 / 180.0 * math.pi), 1, 'sin') assertEquals(math.cos(180 / 180.0 * math.pi), -1, 'cos') assertEquals(math.tan(0 / 180.0 * math.pi), 0, 'tan') @@ -365,7 +364,7 @@ def test_trig(): # Tests the "constant" blocks. def test_constant(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(math.floor(math.pi * 1000), 3141, 'const pi') assertEquals(math.floor(math.e * 1000), 2718, 'const e') assertEquals(math.floor(((1 + math.sqrt(5)) / 2) * 1000), 1618, 'const golden') @@ -394,7 +393,7 @@ def math_isPrime(n): # Tests the "number property" blocks. def test_number_properties(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(42 % 2 == 0, True, 'even') assertEquals(42.1 % 2 == 1, False, 'odd') assertEquals(math_isPrime(5), True, 'prime 5') @@ -411,14 +410,14 @@ def test_number_properties(): # Tests the "round" block. def test_round(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(round(42.42), 42, 'round') assertEquals(math.ceil(-42.42), -42, 'round up') assertEquals(math.floor(42.42), 42, 'round down') # Tests the "change" block. def test_change(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults varToChange = 100 varToChange = (varToChange if isinstance(varToChange, Number) else 0) + 42 assertEquals(varToChange, 142, 'change') @@ -470,7 +469,7 @@ def first_index(my_list, elem): # Tests the "list operation" blocks. def test_operations_on_list(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(sum([3, 4, 5]), 12, 'sum') assertEquals(min([3, 4, 5]), 3, 'min') assertEquals(max([3, 4, 5]), 5, 'max') @@ -483,43 +482,43 @@ def test_operations_on_list(): # Tests the "mod" block. def test_mod(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(42 % 5, 2, 'mod') # Tests the "constrain" block. def test_constraint(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(min(max(100, 0), 42), 42, 'constraint') # Tests the "random integer" block. def test_random_integer(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults rand = random.randint(5, 10) assertEquals(rand >= 5 and rand <= 10, True, 'randRange') assertEquals(rand % 1 == 0, True, 'randInteger') # Tests the "random fraction" block. def test_random_fraction(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults rand = random.random() assertEquals(rand >= 0 and rand <= 1, True, 'randFloat') # Describe this function... def test_atan2(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(math.atan2(5, -5) / math.pi * 180, 135, 'atan2') assertEquals(math.atan2(-12, 0) / math.pi * 180, -90, 'atan2') # Checks that the number of calls is one in order # to confirm that a function was only called once. def check_number_of_calls(test_name): - global naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults test_name = str(test_name) + 'number of calls' assertEquals(number_of_calls, 1, test_name) # Tests the "create text with" block with varying number of inputs. def test_create_text(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals('', '', 'no text') assertEquals('Hello', 'Hello', 'create single') assertEquals(str(-1), '-1', 'create single number') @@ -530,12 +529,12 @@ def test_create_text(): # Creates an empty string for use with the empty test. def get_empty(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults return '' # Tests the "is empty" block". def test_empty_text(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(not len('Google'), False, 'not empty') assertEquals(not len(''), True, 'empty') assertEquals(not len(get_empty()), True, 'empty complex') @@ -543,14 +542,14 @@ def test_empty_text(): # Tests the "length" block. def test_text_length(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(len(''), 0, 'zero length') assertEquals(len('Google'), 6, 'non-zero length') assertEquals(len('car' if True else None), 3, 'length order') # Tests the "append text" block with different types of parameters. def test_append(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults item = 'Miserable' item = str(item) + 'Failure' assertEquals(item, 'MiserableFailure', 'append text') @@ -563,7 +562,7 @@ def test_append(): # Tests the "find" block with a variable. def test_find_text_simple(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults text = 'Banana' assertEquals(text.find('an') + 1, 2, 'find first simple') assertEquals(text.rfind('an') + 1, 4, 'find last simple') @@ -571,13 +570,13 @@ def test_find_text_simple(): # Creates a string for use with the find test. def get_fruit(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults number_of_calls = (number_of_calls if isinstance(number_of_calls, Number) else 0) + 1 return 'Banana' # Tests the "find" block with a function call. def test_find_text_complex(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults number_of_calls = 0 assertEquals(get_fruit().find('an') + 1, 2, 'find first complex') check_number_of_calls('find first complex') @@ -603,7 +602,7 @@ def text_random_letter(text): # Tests the "get letter" block with a variable. def test_get_text_simple(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults text = 'Blockly' assertEquals(text[0], 'B', 'get first simple') assertEquals(text[-1], 'y', 'get last simple') @@ -616,13 +615,13 @@ def test_get_text_simple(): # Creates a string for use with the get test. def get_Blockly(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults number_of_calls = (number_of_calls if isinstance(number_of_calls, Number) else 0) + 1 return 'Blockly' # Tests the "get letter" block with a function call. def test_get_text_complex(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults text = 'Blockly' number_of_calls = 0 assertEquals(get_Blockly()[0], 'B', 'get first complex') @@ -658,13 +657,13 @@ def test_get_text_complex(): # Creates a string for use with the substring test. def get_numbers(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults number_of_calls = (number_of_calls if isinstance(number_of_calls, Number) else 0) + 1 return '123456789' # Tests the "get substring" block with a variable. def test_substring_simple(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults text = '123456789' assertEquals(text[1 : 3], '23', 'substring # simple') assertEquals(text[int((2 if True else None) - 1) : int(3 if True else None)], '23', 'substring # simple order') @@ -685,7 +684,7 @@ def test_substring_simple(): # Tests the "get substring" block with a function call. def test_substring_complex(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults number_of_calls = 0 assertEquals(get_numbers()[1 : 3], '23', 'substring # complex') check_number_of_calls('substring # complex') @@ -733,7 +732,7 @@ def test_substring_complex(): # Tests the "change casing" block. def test_case(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults text = 'Hello World' assertEquals(text.upper(), 'HELLO WORLD', 'uppercase') assertEquals((text if True else None).upper(), 'HELLO WORLD', 'uppercase order') @@ -746,7 +745,7 @@ def test_case(): # Tests the "trim" block. def test_trim(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults text = ' abc def ' assertEquals(text.strip(), 'abc def', 'trim both') assertEquals((text if True else None).strip(), 'abc def', 'trim both order') @@ -757,7 +756,7 @@ def test_trim(): # Tests the "trim" block. def test_count_text(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults text = 'woolloomooloo' assertEquals(text.count('o'), 8, 'len 1') assertEquals(text.count('oo'), 4, 'len 2') @@ -769,7 +768,7 @@ def test_count_text(): # Tests the "trim" block. def test_text_reverse(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(''[::-1], '', 'empty string') assertEquals('a'[::-1], 'a', 'len 1') assertEquals('ab'[::-1], 'ba', 'len 2') @@ -777,7 +776,7 @@ def test_text_reverse(): # Tests the "trim" block. def test_replace(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals('woolloomooloo'.replace('oo', '123'), 'w123ll123m123l123', 'replace all instances 1') assertEquals('woolloomooloo'.replace('.oo', 'X'), 'woolloomooloo', 'literal string replacement') assertEquals('woolloomooloo'.replace('abc', 'X'), 'woolloomooloo', 'not found') @@ -786,30 +785,16 @@ def test_replace(): assertEquals('aaaaa'.replace('a', ''), '', 'empty replacement 3') assertEquals(''.replace('a', 'chicken'), '', 'empty source') -# Tests the "multiline" block. -def test_multiline(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults - assertEquals('', '', 'no text') - assertEquals('Google', 'Google', 'simple') - assertEquals('paragraph' + '\n' + - 'with newlines' + '\n' + - 'yup', 'paragraph' + '\n' + - 'with newlines' + '\n' + - 'yup', 'no compile error with newlines') - assertEquals(('bark bark' + '\n' + - 'bark bark bark' + '\n' + - 'bark bark bark bark').count('bark'), 9, 'count with newlines') - # Checks that the number of calls is one in order # to confirm that a function was only called once. def check_number_of_calls2(test_name): - global naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults test_name = str(test_name) + 'number of calls' assertEquals(number_of_calls, 1, test_name) # Tests the "create list with" and "create empty list" blocks. def test_create_lists(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals([], [], 'create empty') assertEquals([True, 'love'], [True, 'love'], 'create items') assertEquals(['Eject'] * 3, ['Eject', 'Eject', 'Eject'], 'create repeated') @@ -817,12 +802,12 @@ def test_create_lists(): # Creates an empty list for use with the empty test. def get_empty_list(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults return [] # Tests the "is empty" block. def test_lists_empty(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(not len([0]), False, 'not empty') assertEquals(not len([]), True, 'empty') assertEquals(not len(get_empty_list()), True, 'empty complex') @@ -830,7 +815,7 @@ def test_lists_empty(): # Tests the "length" block. def test_lists_length(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(len([]), 0, 'zero length') assertEquals(len(['cat']), 1, 'one length') assertEquals(len(['cat', True, []]), 3, 'three length') @@ -843,7 +828,7 @@ def last_index(my_list, elem): # Tests the "find" block with a variable. def test_find_lists_simple(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = ['Alice', 'Eve', 'Bob', 'Eve'] assertEquals(first_index(list2, 'Eve'), 2, 'find first simple') assertEquals(last_index(list2, 'Eve'), 4, 'find last simple') @@ -851,13 +836,13 @@ def test_find_lists_simple(): # Creates a list for use with the find test. def get_names(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults number_of_calls = (number_of_calls if isinstance(number_of_calls, Number) else 0) + 1 return ['Alice', 'Eve', 'Bob', 'Eve'] # Tests the "find" block with a function call. def test_find_lists_complex(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults number_of_calls = 0 assertEquals(first_index(get_names(), 'Eve'), 2, 'find first complex') check_number_of_calls('find first complex') @@ -879,7 +864,7 @@ def test_find_lists_complex(): # Tests the "get" block with a variable. def test_get_lists_simple(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = ['Kirk', 'Spock', 'McCoy'] assertEquals(list2[0], 'Kirk', 'get first simple') assertEquals(list2[-1], 'McCoy', 'get last simple') @@ -892,7 +877,7 @@ def test_get_lists_simple(): # Tests the "get" block with create list call. def test_get_lists_create_list(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(['Kirk', 'Spock', 'McCoy'][0], 'Kirk', 'get first create list') assertEquals(['Kirk', 'Spock', 'McCoy'][-1], 'McCoy', 'get last simple') assertEquals(first_index(['Kirk', 'Spock', 'McCoy'], random.choice(['Kirk', 'Spock', 'McCoy'])) > 0, True, 'get random simple') @@ -904,13 +889,13 @@ def test_get_lists_create_list(): # Creates a list for use with the get test. def get_star_wars(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults number_of_calls = (number_of_calls if isinstance(number_of_calls, Number) else 0) + 1 return ['Kirk', 'Spock', 'McCoy'] # Tests the "get" block with a function call. def test_get_lists_complex(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = ['Kirk', 'Spock', 'McCoy'] number_of_calls = 0 assertEquals(get_star_wars()[0], 'Kirk', 'get first complex') @@ -950,7 +935,7 @@ def lists_remove_random_item(myList): # Tests the "get and remove" block. def test_getRemove(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = ['Kirk', 'Spock', 'McCoy'] assertEquals(list2.pop(0), 'Kirk', 'getremove first') assertEquals(list2, ['Spock', 'McCoy'], 'getremove first list') @@ -985,7 +970,7 @@ def test_getRemove(): # Tests the "remove" block. def test_remove(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = ['Kirk', 'Spock', 'McCoy'] list2.pop(0) assertEquals(list2, ['Spock', 'McCoy'], 'remove first list') @@ -1021,7 +1006,7 @@ def test_remove(): # Tests the "set" block. def test_set(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = ['Picard', 'Riker', 'Crusher'] list2[0] = 'Jean-Luc' assertEquals(list2, ['Jean-Luc', 'Riker', 'Crusher'], 'set first list') @@ -1060,7 +1045,7 @@ def test_set(): # Tests the "insert" block. def test_insert(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = ['Picard', 'Riker', 'Crusher'] list2.insert(0, 'Data') assertEquals(list2, ['Data', 'Picard', 'Riker', 'Crusher'], 'insert first list') @@ -1099,7 +1084,7 @@ def test_insert(): # Tests the "get sub-list" block with a variable. def test_sublist_simple(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = ['Columbia', 'Challenger', 'Discovery', 'Atlantis', 'Endeavour'] assertEquals(list2[1 : 3], ['Challenger', 'Discovery'], 'sublist # simple') assertEquals(list2[int((2 if True else None) - 1) : int(3 if True else None)], ['Challenger', 'Discovery'], 'sublist # simple order') @@ -1124,13 +1109,13 @@ def test_sublist_simple(): # Creates a list for use with the sublist test. def get_space_shuttles(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults number_of_calls = (number_of_calls if isinstance(number_of_calls, Number) else 0) + 1 return ['Columbia', 'Challenger', 'Discovery', 'Atlantis', 'Endeavour'] # Tests the "get sub-list" block with a function call. def test_sublist_complex(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults number_of_calls = 0 assertEquals(get_space_shuttles()[1 : 3], ['Challenger', 'Discovery'], 'sublist # start complex') check_number_of_calls('sublist # start complex') @@ -1178,14 +1163,14 @@ def test_sublist_complex(): # Tests the "join" block. def test_join(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = ['Vulcan', 'Klingon', 'Borg'] assertEquals(','.join(list2), 'Vulcan,Klingon,Borg', 'join') assertEquals(','.join(list2 if True else None), 'Vulcan,Klingon,Borg', 'join order') # Tests the "split" block. def test_split(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults text = 'Vulcan,Klingon,Borg' assertEquals(text.split(','), ['Vulcan', 'Klingon', 'Borg'], 'split') assertEquals((text if True else None).split(','), ['Vulcan', 'Klingon', 'Borg'], 'split order') @@ -1207,78 +1192,37 @@ def lists_sort(my_list, type, reverse): # Tests the "alphabetic sort" block. def test_sort_alphabetic(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = ['Vulcan', 'klingon', 'Borg'] assertEquals(lists_sort(list2, "TEXT", False), ['Borg', 'Vulcan', 'klingon'], 'sort alphabetic ascending') assertEquals(lists_sort(list2 if True else None, "TEXT", False), ['Borg', 'Vulcan', 'klingon'], 'sort alphabetic ascending order') # Tests the "alphabetic sort ignore case" block. def test_sort_ignoreCase(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = ['Vulcan', 'klingon', 'Borg'] assertEquals(lists_sort(list2, "IGNORE_CASE", False), ['Borg', 'klingon', 'Vulcan'], 'sort ignore case ascending') assertEquals(lists_sort(list2 if True else None, "IGNORE_CASE", False), ['Borg', 'klingon', 'Vulcan'], 'sort ignore case ascending order') # Tests the "numeric sort" block. def test_sort_numeric(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = [8, 18, -1] assertEquals(lists_sort(list2, "NUMERIC", True), [18, 8, -1], 'sort numeric descending') assertEquals(lists_sort(list2 if True else None, "NUMERIC", True), [18, 8, -1], 'sort numeric descending order') # Tests the "list reverse" block. def test_lists_reverse(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults list2 = [8, 18, -1, 64] assertEquals(list(reversed(list2)), [64, -1, 18, 8], 'reverse a copy') assertEquals(list2, [8, 18, -1, 64], 'reverse a copy original') list2 = [] assertEquals(list(reversed(list2)), [], 'empty list') -# Describe this function... -def test_colour_picker(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults - assertEquals('#ff6600', '#ff6600', 'static colour') - -def colour_rgb(r, g, b): - r = round(min(100, max(0, r)) * 2.55) - g = round(min(100, max(0, g)) * 2.55) - b = round(min(100, max(0, b)) * 2.55) - return '#%02x%02x%02x' % (r, g, b) - -# Describe this function... -def test_rgb(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults - assertEquals(colour_rgb(100, 40, 0), '#ff6600', 'from rgb') - -# Describe this function... -def test_colour_random(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults - for count4 in range(100): - item = '#%06x' % random.randint(0, 2**24 - 1) - assertEquals(len(item), 7, 'length of random colour string: ' + str(item)) - assertEquals(item[0], '#', 'format of random colour string: ' + str(item)) - for i in range(1, 7): - assertEquals(0 != 'abcdefABDEF0123456789'.find(item[int((i + 1) - 1)]) + 1, True, ''.join([str(x4) for x4 in ['contents of random colour string: ', item, ' at index: ', i + 1]])) - -def colour_blend(colour1, colour2, ratio): - r1, r2 = int(colour1[1:3], 16), int(colour2[1:3], 16) - g1, g2 = int(colour1[3:5], 16), int(colour2[3:5], 16) - b1, b2 = int(colour1[5:7], 16), int(colour2[5:7], 16) - ratio = min(1, max(0, ratio)) - r = round(r1 * (1 - ratio) + r2 * ratio) - g = round(g1 * (1 - ratio) + g2 * ratio) - b = round(b1 * (1 - ratio) + b2 * ratio) - return '#%02x%02x%02x' % (r, g, b) - -# Describe this function... -def test_blend(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults - assertEquals(colour_blend('#ff0000', colour_rgb(100, 40, 0), 0.4), '#ff2900', 'blend') - # Describe this function... def test_procedure(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults procedure_1(8, 2) assertEquals(proc_z, 4, 'procedure with global') proc_w = False @@ -1290,19 +1234,19 @@ def test_procedure(): # Describe this function... def procedure_1(proc_x, proc_y): - global test_name, naked, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults proc_z = proc_x / proc_y # Describe this function... def procedure_2(proc_x): - global test_name, naked, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults if proc_x: return proc_w = True # Describe this function... def test_function(): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults assertEquals(function_1(2, 3), -1, 'function with arguments') assertEquals(func_z, 'side effect', 'function with side effect') func_a = 'unchanged' @@ -1314,28 +1258,28 @@ def test_function(): # Describe this function... def function_1(func_x, func_y): - global test_name, naked, proc_x, proc_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_a, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults func_z = 'side effect' return func_x - func_y # Describe this function... def function_2(func_a): - global test_name, naked, proc_x, proc_y, func_x, func_y, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults func_a = (func_a if isinstance(func_a, Number) else 0) + 1 return str(func_a) + str(func_c) # Describe this function... def function_3(func_a): - global test_name, naked, proc_x, proc_y, func_x, func_y, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, n, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults if func_a: return True return False # Describe this function... def recurse(n): - global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, i, loglist, changing_list, list_copy, unittestResults + global test_name, naked, proc_x, proc_y, func_x, func_y, func_a, ok, log, count, varToChange, rand, item, text, number_of_calls, list2, proc_z, func_z, x, proc_w, func_c, if2, loglist, changing_list, list_copy, unittestResults if n > 0: - text = ''.join([str(x5) for x5 in [recurse(n - 1), n, recurse(n - 1)]]) + text = ''.join([str(x4) for x4 in [recurse(n - 1), n, recurse(n - 1)]]) else: text = '-' return text @@ -1414,7 +1358,6 @@ test_trim() test_count_text() test_text_reverse() test_replace() -test_multiline() print(unittest_report()) unittestResults = None @@ -1443,15 +1386,6 @@ test_lists_reverse() print(unittest_report()) unittestResults = None -unittestResults = [] -print('\n====================\n\nRunning suite: Colour') -test_colour_picker() -test_blend() -test_rgb() -test_colour_random() -print(unittest_report()) -unittestResults = None - unittestResults = [] print('\n====================\n\nRunning suite: Variables') item = 123 diff --git a/tests/generators/index.html b/tests/generators/index.html index cb30c0966..9ec1db773 100644 --- a/tests/generators/index.html +++ b/tests/generators/index.html @@ -321,7 +321,6 @@ h1 { - @@ -337,12 +336,6 @@ h1 { - - - - - - @@ -364,7 +357,6 @@ h1 { Math
Text
Lists
- Colour
Variables
Functions
diff --git a/tests/generators/text.xml b/tests/generators/text.xml index d10593735..61e961aef 100644 --- a/tests/generators/text.xml +++ b/tests/generators/text.xml @@ -46,11 +46,6 @@ - - - - - @@ -4653,93 +4648,4 @@ - - test multiline - Tests the "multiline" block. - - - - - no text - - - - - - - - - - - - - - - - - simple - - - - - Google - - - - - Google - - - - - - - no compile error with newlines - - - - - paragraph with newlines yup - - - - - paragraph with newlines yup - - - - - - - count with newlines - - - - - - - bark - - - - - bark bark bark bark bark bark bark bark bark - - - - - - - 9 - - - - - - - - - - - \ No newline at end of file diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index f5b2e6a82..e158391fc 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -19,6 +19,7 @@ import { createMockEvent, } from './test_helpers/events.js'; import {MockIcon, MockBubbleIcon} from './test_helpers/icon_mocks.js'; +import {IconType} from '../../build/src/core/icons/icon_types.js'; suite('Blocks', function () { setup(function () { @@ -67,9 +68,9 @@ suite('Blocks', function () { function createTestBlocks(workspace, isRow) { const blockType = isRow ? 'row_block' : 'stack_block'; - const blockA = workspace.newBlock(blockType); - const blockB = workspace.newBlock(blockType); - const blockC = workspace.newBlock(blockType); + const blockA = workspace.newBlock(blockType, 'a'); + const blockB = workspace.newBlock(blockType, 'b'); + const blockC = workspace.newBlock(blockType, 'c'); if (isRow) { blockA.inputList[0].connection.connect(blockB.outputConnection); @@ -386,8 +387,14 @@ suite('Blocks', function () { test('Child is shadow', function () { const blocks = this.blocks; - blocks.C.setShadow(true); + blocks.C.dispose(); + blocks.B.inputList[0].connection.setShadowState({ + 'type': 'row_block', + 'id': 'c', + }); + blocks.B.dispose(true); + // Even though we're asking to heal, it will appear as if it has not // healed because shadows always get destroyed. assertDisposedNoheal(blocks); @@ -423,8 +430,14 @@ suite('Blocks', function () { test('Child is shadow', function () { const blocks = this.blocks; - blocks.C.setShadow(true); + blocks.C.dispose(); + blocks.B.nextConnection.setShadowState({ + 'type': 'stack_block', + 'id': 'c', + }); + blocks.B.dispose(true); + // Even though we're asking to heal, it will appear as if it has not // healed because shadows always get destroyed. assertDisposedNoheal(blocks); @@ -1355,6 +1368,99 @@ suite('Blocks', function () { }); }); }); + + suite('Constructing registered comment classes', function () { + class MockComment extends MockIcon { + getType() { + return Blockly.icons.IconType.COMMENT; + } + + setText() {} + + getText() { + return ''; + } + + setBubbleSize() {} + + getBubbleSize() { + return Blockly.utils.Size(0, 0); + } + + bubbleIsVisible() { + return true; + } + + setBubbleVisible() {} + + saveState() { + return {}; + } + + loadState() {} + } + + setup(function () { + this.workspace = Blockly.inject('blocklyDiv', {}); + + this.block = this.workspace.newBlock('stack_block'); + this.block.initSvg(); + this.block.render(); + }); + + teardown(function () { + workspaceTeardown.call(this, this.workspace); + + Blockly.icons.registry.unregister( + Blockly.icons.IconType.COMMENT.toString(), + ); + Blockly.icons.registry.register( + Blockly.icons.IconType.COMMENT, + Blockly.icons.CommentIcon, + ); + }); + + test('setCommentText constructs the registered comment icon', function () { + Blockly.icons.registry.unregister( + Blockly.icons.IconType.COMMENT.toString(), + ); + Blockly.icons.registry.register( + Blockly.icons.IconType.COMMENT, + MockComment, + ); + + this.block.setCommentText('test text'); + + chai.assert.instanceOf( + this.block.getIcon(Blockly.icons.IconType.COMMENT), + MockComment, + ); + }); + + test('setCommentText throws if no icon is registered', function () { + Blockly.icons.registry.unregister( + Blockly.icons.IconType.COMMENT.toString(), + ); + + chai.assert.throws(() => { + this.block.setCommentText('test text'); + }, 'No comment icon class is registered, so a comment cannot be set'); + }); + + test('setCommentText throws if the icon is not an ICommentIcon', function () { + Blockly.icons.registry.unregister( + Blockly.icons.IconType.COMMENT.toString(), + ); + Blockly.icons.registry.register( + Blockly.icons.IconType.COMMENT, + MockIcon, + ); + + chai.assert.throws(() => { + this.block.setCommentText('test text'); + }, 'The class registered as a comment icon does not conform to the ICommentIcon interface'); + }); + }); }); suite('Getting/Setting Field (Values)', function () { @@ -2207,15 +2313,15 @@ suite('Blocks', function () { .getInput('STATEMENT') .connection.connect(blockB.previousConnection); // Disable the block and collapse it. - blockA.setEnabled(false); + blockA.setDisabledReason(true, 'test reason'); blockA.setCollapsed(true); // Enable the block before expanding it. - blockA.setEnabled(true); + blockA.setDisabledReason(false, 'test reason'); blockA.setCollapsed(false); // The child blocks should be enabled. - chai.assert.isFalse(blockB.disabled); + chai.assert.isTrue(blockB.isEnabled()); chai.assert.isFalse( blockB.getSvgRoot().classList.contains('blocklyDisabled'), ); @@ -2228,18 +2334,18 @@ suite('Blocks', function () { .connection.connect(blockB.previousConnection); // Disable the child block. - blockB.setEnabled(false); + blockB.setDisabledReason(true, 'test reason'); // Collapse and disable the parent block. blockA.setCollapsed(false); - blockA.setEnabled(false); + blockA.setDisabledReason(true, 'test reason'); // Enable the parent block. - blockA.setEnabled(true); + blockA.setDisabledReason(false, 'test reason'); blockA.setCollapsed(true); // Child blocks should stay disabled if they have been set. - chai.assert.isTrue(blockB.disabled); + chai.assert.isFalse(blockB.isEnabled()); }); test('Disabled blocks from JSON should have proper disabled status', function () { // Nested c-shaped blocks, inner block is disabled @@ -2334,7 +2440,7 @@ suite('Blocks', function () { this.child4 = this.workspace.getBlockById('child4'); }); test('Disabling parent block visually disables all descendants', async function () { - this.parent.setEnabled(false); + this.parent.setDisabledReason(true, 'test reason'); await Blockly.renderManagement.finishQueuedRenders(); for (const child of this.parent.getDescendants(false)) { chai.assert.isTrue( @@ -2344,9 +2450,9 @@ suite('Blocks', function () { } }); test('Child blocks regain original status after parent is re-enabled', async function () { - this.parent.setEnabled(false); + this.parent.setDisabledReason(true, 'test reason'); await Blockly.renderManagement.finishQueuedRenders(); - this.parent.setEnabled(true); + this.parent.setDisabledReason(false, 'test reason'); await Blockly.renderManagement.finishQueuedRenders(); // child2 is disabled, rest should be enabled diff --git a/tests/mocha/blocks/loops_test.js b/tests/mocha/blocks/loops_test.js new file mode 100644 index 000000000..3bbfdac10 --- /dev/null +++ b/tests/mocha/blocks/loops_test.js @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from '../../../build/src/core/blockly.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from '../test_helpers/setup_teardown.js'; + +suite('Loops', function () { + setup(function () { + sharedTestSetup.call(this, {fireEventsNow: false}); + this.workspace = Blockly.inject('blocklyDiv', {}); + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + suite('controls_flow_statements blocks', function () { + test('break block is invalid outside of loop block', function () { + const breakBlock = Blockly.serialization.blocks.append( + {'type': 'controls_flow_statements'}, + this.workspace, + ); + this.clock.runAll(); + chai.assert.isFalse( + breakBlock.isEnabled(), + 'Expected the break block to be disabled', + ); + }); + + test('break block is valid inside of loop block', function () { + const loopBlock = Blockly.serialization.blocks.append( + {'type': 'controls_repeat'}, + this.workspace, + ); + const breakBlock = Blockly.serialization.blocks.append( + {'type': 'controls_flow_statements'}, + this.workspace, + ); + loopBlock + .getInput('DO') + .connection.connect(breakBlock.previousConnection); + this.clock.runAll(); + chai.assert.isTrue( + breakBlock.isEnabled(), + 'Expected the break block to be enabled', + ); + }); + }); +}); diff --git a/tests/mocha/blocks/procedures_test.js b/tests/mocha/blocks/procedures_test.js index 6173179cd..109d3b2d4 100644 --- a/tests/mocha/blocks/procedures_test.js +++ b/tests/mocha/blocks/procedures_test.js @@ -86,10 +86,10 @@ suite('Procedures', function () { }); suite('adding procedure parameters', function () { - test('the mutator flyout updates to avoid parameter name conflicts', function () { + test('the mutator flyout updates to avoid parameter name conflicts', async function () { const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const origFlyoutParamName = mutatorWorkspace .getFlyout() @@ -119,11 +119,11 @@ suite('Procedures', function () { ); }); - test('adding a parameter to the procedure updates procedure defs', function () { + test('adding a parameter to the procedure updates procedure defs', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -143,12 +143,12 @@ suite('Procedures', function () { ); }); - test('adding a parameter to the procedure updates procedure callers', function () { + test('adding a parameter to the procedure updates procedure callers', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -169,11 +169,11 @@ suite('Procedures', function () { ); }); - test('undoing adding a procedure parameter removes it', function () { + test('undoing adding a procedure parameter removes it', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -194,11 +194,11 @@ suite('Procedures', function () { test( 'undoing and redoing adding a procedure parameter maintains ' + 'the same state', - function () { + async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -224,11 +224,11 @@ suite('Procedures', function () { }); suite('deleting procedure parameters', function () { - test('deleting a parameter from the procedure updates procedure defs', function () { + test('deleting a parameter from the procedure updates procedure defs', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -247,12 +247,12 @@ suite('Procedures', function () { ); }); - test('deleting a parameter from the procedure udpates procedure callers', function () { + test('deleting a parameter from the procedure udpates procedure callers', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -271,11 +271,11 @@ suite('Procedures', function () { ); }); - test('undoing deleting a procedure parameter adds it', function () { + test('undoing deleting a procedure parameter adds it', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -298,11 +298,11 @@ suite('Procedures', function () { test( 'undoing and redoing deleting a procedure parameter maintains ' + 'the same state', - function () { + async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -326,11 +326,11 @@ suite('Procedures', function () { }); suite('renaming procedure parameters', function () { - test('defs are updated for parameter renames', function () { + test('defs are updated for parameter renames', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -353,11 +353,11 @@ suite('Procedures', function () { ); }); - test('defs are updated for parameter renames when two params exist', function () { + test('defs are updated for parameter renames when two params exist', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock1 = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -383,12 +383,12 @@ suite('Procedures', function () { ); }); - test('callers are updated for parameter renames', function () { + test('callers are updated for parameter renames', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -412,12 +412,12 @@ suite('Procedures', function () { ); }); - test('variables associated with procedure parameters are not renamed', function () { + test('variables associated with procedure parameters are not renamed', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -436,11 +436,11 @@ suite('Procedures', function () { ); }); - test('renaming a variable associated with a parameter updates procedure defs', function () { + test('renaming a variable associated with a parameter updates procedure defs', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -464,11 +464,11 @@ suite('Procedures', function () { ); }); - test('renaming a variable associated with a parameter updates mutator parameters', function () { + test('renaming a variable associated with a parameter updates mutator parameters', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -488,12 +488,12 @@ suite('Procedures', function () { ); }); - test('renaming a variable associated with a parameter updates procedure callers', function () { + test('renaming a variable associated with a parameter updates procedure callers', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -518,11 +518,11 @@ suite('Procedures', function () { ); }); - test('coalescing a variable associated with a parameter updates procedure defs', function () { + test('coalescing a variable associated with a parameter updates procedure defs', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -546,11 +546,11 @@ suite('Procedures', function () { ); }); - test('coalescing a variable associated with a parameter updates mutator parameters', function () { + test('coalescing a variable associated with a parameter updates mutator parameters', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -570,12 +570,12 @@ suite('Procedures', function () { ); }); - test('coalescing a variable associated with a parameter updates procedure callers', function () { + test('coalescing a variable associated with a parameter updates procedure callers', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -606,11 +606,11 @@ suite('Procedures', function () { function () {}, ); - test('undoing renaming a procedure parameter reverts the change', function () { + test('undoing renaming a procedure parameter reverts the change', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -637,11 +637,11 @@ suite('Procedures', function () { ); }); - test('undoing and redoing renaming a procedure maintains the same state', function () { + test('undoing and redoing renaming a procedure maintains the same state', async function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -670,11 +670,11 @@ suite('Procedures', function () { }); suite('reordering procedure parameters', function () { - test('reordering procedure parameters updates procedure blocks', function () { + test('reordering procedure parameters updates procedure blocks', async function () { // Create a stack of container, parameter, parameter. const defBlock = createProcDefBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock1 = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -706,12 +706,12 @@ suite('Procedures', function () { ); }); - test('reordering procedure parameters updates caller blocks', function () { + test('reordering procedure parameters updates caller blocks', async function () { // Create a stack of container, parameter, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock1 = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -756,12 +756,12 @@ suite('Procedures', function () { test( 'reordering procedure parameters reorders the blocks ' + 'attached to caller inputs', - function () { + async function () { // Create a stack of container, parameter, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock1 = mutatorWorkspace.newBlock('procedures_mutatorarg'); @@ -811,7 +811,7 @@ suite('Procedures', function () { const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); - defBlock.setEnabled(false); + defBlock.setDisabledReason(true, 'MANUALLY_DISABLED'); this.clock.runAll(); chai.assert.isFalse( @@ -821,16 +821,33 @@ suite('Procedures', function () { }, ); + test( + 'if a procedure definition is invalid, the procedure caller ' + + 'is also invalid', + function () { + const defBlock = createProcDefBlock(this.workspace); + const callBlock = createProcCallBlock(this.workspace); + + defBlock.setDisabledReason(true, 'test reason'); + this.clock.runAll(); + + chai.assert.isFalse( + callBlock.isEnabled(), + 'Expected the caller block to be invalid', + ); + }, + ); + test( 'if a procedure definition is enabled, the procedure caller ' + 'is also enabled', function () { const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); - defBlock.setEnabled(false); + defBlock.setDisabledReason(true, 'MANUALLY_DISABLED'); this.clock.runAll(); - defBlock.setEnabled(true); + defBlock.setDisabledReason(false, 'MANUALLY_DISABLED'); this.clock.runAll(); chai.assert.isTrue( @@ -847,12 +864,12 @@ suite('Procedures', function () { const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); this.clock.runAll(); - callBlock.setEnabled(false); + callBlock.setDisabledReason(true, 'MANUALLY_DISABLED'); this.clock.runAll(); - defBlock.setEnabled(false); + defBlock.setDisabledReason(true, 'MANUALLY_DISABLED'); this.clock.runAll(); - defBlock.setEnabled(true); + defBlock.setDisabledReason(false, 'MANUALLY_DISABLED'); this.clock.runAll(); chai.assert.isFalse( @@ -863,6 +880,36 @@ suite('Procedures', function () { ); }); + suite('procedures_ifreturn blocks', function () { + test('ifreturn block is invalid outside of def block', function () { + const ifreturnBlock = Blockly.serialization.blocks.append( + {'type': 'procedures_ifreturn'}, + this.workspace, + ); + this.clock.runAll(); + chai.assert.isFalse( + ifreturnBlock.isEnabled(), + 'Expected the ifreturn block to be invalid', + ); + }); + + test('ifreturn block is valid inside of def block', function () { + const defBlock = createProcDefBlock(this.workspace); + const ifreturnBlock = Blockly.serialization.blocks.append( + {'type': 'procedures_ifreturn'}, + this.workspace, + ); + defBlock + .getInput('STACK') + .connection.connect(ifreturnBlock.previousConnection); + this.clock.runAll(); + chai.assert.isTrue( + ifreturnBlock.isEnabled(), + 'Expected the ifreturn block to be valid', + ); + }); + }); + suite('deleting procedure blocks', function () { test( 'when the procedure definition block is deleted, all of its ' + @@ -1909,11 +1956,11 @@ suite('Procedures', function () { } }); suite('Untyped Arguments', function () { - function createMutator(argArray) { + async function createMutator(argArray) { const mutatorIcon = this.defBlock.getIcon( Blockly.icons.MutatorIcon.TYPE, ); - mutatorIcon.setBubbleVisible(true); + await mutatorIcon.setBubbleVisible(true); this.mutatorWorkspace = mutatorIcon.getWorkspace(); this.containerBlock = this.mutatorWorkspace.getTopBlocks()[0]; this.connection = @@ -1946,58 +1993,58 @@ suite('Procedures', function () { chai.assert.equal(this.callBlock.getVars()[i], argArray[i]); } } - test('Simple Add Arg', function () { + test('Simple Add Arg', async function () { const args = ['arg1']; - createMutator.call(this, args); + await createMutator.call(this, args); assertArgs.call(this, args); }); - test('Multiple Args', function () { + test('Multiple Args', async function () { const args = ['arg1', 'arg2', 'arg3']; - createMutator.call(this, args); + await createMutator.call(this, args); assertArgs.call(this, args); }); - test('Simple Change Arg', function () { - createMutator.call(this, ['arg1']); + test('Simple Change Arg', async function () { + await createMutator.call(this, ['arg1']); this.argBlock.setFieldValue('arg2', 'NAME'); this.defBlock.compose(this.containerBlock); assertArgs.call(this, ['arg2']); }); - test('lower -> CAPS', function () { - createMutator.call(this, ['arg']); + test('lower -> CAPS', async function () { + await createMutator.call(this, ['arg']); this.argBlock.setFieldValue('ARG', 'NAME'); this.defBlock.compose(this.containerBlock); assertArgs.call(this, ['ARG']); }); - test('CAPS -> lower', function () { - createMutator.call(this, ['ARG']); + test('CAPS -> lower', async function () { + await createMutator.call(this, ['ARG']); this.argBlock.setFieldValue('arg', 'NAME'); this.defBlock.compose(this.containerBlock); assertArgs.call(this, ['arg']); }); // Test case for #1958 - test('Set Arg Empty', function () { + test('Set Arg Empty', async function () { const args = ['arg1']; - createMutator.call(this, args); + await createMutator.call(this, args); this.argBlock.setFieldValue('', 'NAME'); this.defBlock.compose(this.containerBlock); assertArgs.call(this, args); }); - test('Whitespace', function () { + test('Whitespace', async function () { const args = ['arg1']; - createMutator.call(this, args); + await createMutator.call(this, args); this.argBlock.setFieldValue(' ', 'NAME'); this.defBlock.compose(this.containerBlock); assertArgs.call(this, args); }); - test('Whitespace and Text', function () { - createMutator.call(this, ['arg1']); + test('Whitespace and Text', async function () { + await createMutator.call(this, ['arg1']); this.argBlock.setFieldValue(' text ', 'NAME'); this.defBlock.compose(this.containerBlock); assertArgs.call(this, ['text']); }); - test('<>', function () { + test('<>', async function () { const args = ['<>']; - createMutator.call(this, args); + await createMutator.call(this, args); assertArgs.call(this, args); }); }); diff --git a/tests/mocha/clipboard_test.js b/tests/mocha/clipboard_test.js index f134b1d77..fb0c41882 100644 --- a/tests/mocha/clipboard_test.js +++ b/tests/mocha/clipboard_test.js @@ -114,7 +114,8 @@ suite('Clipboard', function () { }); suite('pasting comments', function () { - test('pasted comments are bumped to not overlap', function () { + // TODO: Reenable test when we readd copy-paste. + test.skip('pasted comments are bumped to not overlap', function () { Blockly.Xml.domToWorkspace( Blockly.utils.xml.textToDom( '', diff --git a/tests/mocha/comment_deserialization_test.js b/tests/mocha/comment_deserialization_test.js index 494c584e7..843453278 100644 --- a/tests/mocha/comment_deserialization_test.js +++ b/tests/mocha/comment_deserialization_test.js @@ -74,9 +74,7 @@ suite('Comment Deserialization', function () { simulateClick(this.workspace.trashcan.svgGroup); // Place from trashcan. simulateClick( - this.workspace.trashcan.flyout.svgGroup_.querySelector( - '.blocklyDraggable', - ), + this.workspace.trashcan.flyout.svgGroup_.querySelector('.blocklyPath'), ); chai.assert.equal(this.workspace.getAllBlocks().length, 1); // Check comment. @@ -113,7 +111,7 @@ suite('Comment Deserialization', function () { const toolbox = this.workspace.getToolbox(); simulateClick(toolbox.HtmlDiv.querySelector('.blocklyTreeRow')); simulateClick( - toolbox.getFlyout().svgGroup_.querySelector('.blocklyDraggable'), + toolbox.getFlyout().svgGroup_.querySelector('.blocklyPath'), ); chai.assert.equal(this.workspace.getAllBlocks().length, 1); // Check comment. diff --git a/tests/mocha/comment_test.js b/tests/mocha/comment_test.js index 6f19aa7f0..452f07493 100644 --- a/tests/mocha/comment_test.js +++ b/tests/mocha/comment_test.js @@ -47,8 +47,8 @@ suite('Comments', function () { chai.assert.isNotOk(comment.textInputBubble); chai.assert.isOk(comment.textBubble); } - test('Editable', function () { - this.comment.setBubbleVisible(true); + test('Editable', async function () { + await this.comment.setBubbleVisible(true); chai.assert.isTrue(this.comment.bubbleIsVisible()); assertEditable(this.comment); assertEventFired( @@ -59,10 +59,10 @@ suite('Comments', function () { this.block.id, ); }); - test('Not Editable', function () { + test('Not Editable', async function () { sinon.stub(this.block, 'isEditable').returns(false); - this.comment.setBubbleVisible(true); + await this.comment.setBubbleVisible(true); chai.assert.isTrue(this.comment.bubbleIsVisible()); assertNotEditable(this.comment); @@ -74,11 +74,11 @@ suite('Comments', function () { this.block.id, ); }); - test('Editable -> Not Editable', function () { - this.comment.setBubbleVisible(true); + test('Editable -> Not Editable', async function () { + await this.comment.setBubbleVisible(true); sinon.stub(this.block, 'isEditable').returns(false); - this.comment.updateEditable(); + await this.comment.updateEditable(); chai.assert.isTrue(this.comment.bubbleIsVisible()); assertNotEditable(this.comment); @@ -90,14 +90,14 @@ suite('Comments', function () { this.block.id, ); }); - test('Not Editable -> Editable', function () { + test('Not Editable -> Editable', async function () { const editableStub = sinon.stub(this.block, 'isEditable').returns(false); - this.comment.setBubbleVisible(true); + await this.comment.setBubbleVisible(true); editableStub.returns(true); - this.comment.updateEditable(); + await this.comment.updateEditable(); chai.assert.isTrue(this.comment.bubbleIsVisible()); assertEditable(this.comment); assertEventFired( diff --git a/tests/mocha/comment_view_test.js b/tests/mocha/comment_view_test.js new file mode 100644 index 000000000..6650848e5 --- /dev/null +++ b/tests/mocha/comment_view_test.js @@ -0,0 +1,199 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Workspace comment', function () { + setup(function () { + sharedTestSetup.call(this); + this.workspace = new Blockly.inject('blocklyDiv', {}); + this.commentView = new Blockly.comments.CommentView(this.workspace); + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + suite('Listeners', function () { + suite('Text change listeners', function () { + test('text change listeners are called when text is changed', function () { + const spy = sinon.spy(); + this.commentView.addTextChangeListener(spy); + + this.commentView.setText('test'); + + chai.assert.isTrue( + spy.calledOnce, + 'Expected the spy to be called once', + ); + chai.assert.isTrue( + spy.calledWith('', 'test'), + 'Expected the spy to be called with the given args', + ); + }); + + test('text change listeners can remove themselves without skipping others', function () { + const fake1 = sinon.fake(); + const fake2 = sinon.fake(() => + this.commentView.removeTextChangeListener(fake2), + ); + const fake3 = sinon.fake(); + this.commentView.addTextChangeListener(fake1); + this.commentView.addTextChangeListener(fake2); + this.commentView.addTextChangeListener(fake3); + + this.commentView.setText('test'); + + chai.assert.isTrue( + fake1.calledOnce, + 'Expected the first listener to be called', + ); + chai.assert.isTrue( + fake2.calledOnce, + 'Expected the second listener to be called', + ); + chai.assert.isTrue( + fake3.calledOnce, + 'Expected the third listener to be called', + ); + }); + }); + + suite('Size change listeners', function () { + test('size change listeners are called when text is changed', function () { + const spy = sinon.spy(); + this.commentView.addSizeChangeListener(spy); + const originalSize = this.commentView.getSize(); + const newSize = new Blockly.utils.Size(1337, 1337); + + this.commentView.setSize(newSize); + + chai.assert.isTrue( + spy.calledOnce, + 'Expected the spy to be called once', + ); + chai.assert.isTrue( + spy.calledWith(originalSize, newSize), + 'Expected the spy to be called with the given args', + ); + }); + + test('size change listeners can remove themselves without skipping others', function () { + const fake1 = sinon.fake(); + const fake2 = sinon.fake(() => + this.commentView.removeSizeChangeListener(fake2), + ); + const fake3 = sinon.fake(); + this.commentView.addSizeChangeListener(fake1); + this.commentView.addSizeChangeListener(fake2); + this.commentView.addSizeChangeListener(fake3); + const newSize = new Blockly.utils.Size(1337, 1337); + + this.commentView.setSize(newSize); + + chai.assert.isTrue( + fake1.calledOnce, + 'Expected the first listener to be called', + ); + chai.assert.isTrue( + fake2.calledOnce, + 'Expected the second listener to be called', + ); + chai.assert.isTrue( + fake3.calledOnce, + 'Expected the third listener to be called', + ); + }); + }); + + suite('Collapse change listeners', function () { + test('collapse change listeners are called when text is changed', function () { + const spy = sinon.spy(); + this.commentView.addOnCollapseListener(spy); + + this.commentView.setCollapsed(true); + + chai.assert.isTrue( + spy.calledOnce, + 'Expected the spy to be called once', + ); + chai.assert.isTrue( + spy.calledWith(true), + 'Expected the spy to be called with the given args', + ); + }); + + test('collapse change listeners can remove themselves without skipping others', function () { + const fake1 = sinon.fake(); + const fake2 = sinon.fake(() => + this.commentView.removeOnCollapseListener(fake2), + ); + const fake3 = sinon.fake(); + this.commentView.addOnCollapseListener(fake1); + this.commentView.addOnCollapseListener(fake2); + this.commentView.addOnCollapseListener(fake3); + + this.commentView.setCollapsed(true); + + chai.assert.isTrue( + fake1.calledOnce, + 'Expected the first listener to be called', + ); + chai.assert.isTrue( + fake2.calledOnce, + 'Expected the second listener to be called', + ); + chai.assert.isTrue( + fake3.calledOnce, + 'Expected the third listener to be called', + ); + }); + }); + + suite('Dispose change listeners', function () { + test('dispose listeners are called when text is changed', function () { + const spy = sinon.spy(); + this.commentView.addDisposeListener(spy); + + this.commentView.dispose(); + + chai.assert.isTrue( + spy.calledOnce, + 'Expected the spy to be called once', + ); + }); + + test('dispose listeners can remove themselves without skipping others', function () { + const fake1 = sinon.fake(); + const fake2 = sinon.fake(() => + this.commentView.removeDisposeListener(fake2), + ); + const fake3 = sinon.fake(); + this.commentView.addDisposeListener(fake1); + this.commentView.addDisposeListener(fake2); + this.commentView.addDisposeListener(fake3); + + this.commentView.dispose(); + + chai.assert.isTrue( + fake1.calledOnce, + 'Expected the first listener to be called', + ); + chai.assert.isTrue( + fake2.calledOnce, + 'Expected the second listener to be called', + ); + chai.assert.isTrue( + fake3.calledOnce, + 'Expected the third listener to be called', + ); + }); + }); + }); +}); diff --git a/tests/mocha/event_comment_change_test.js b/tests/mocha/event_comment_change_test.js index 7c68d0858..c2355f874 100644 --- a/tests/mocha/event_comment_change_test.js +++ b/tests/mocha/event_comment_change_test.js @@ -21,12 +21,8 @@ suite('Comment Change Event', function () { suite('Serialization', function () { test('events round-trip through JSON', function () { - const comment = new Blockly.WorkspaceComment( - this.workspace, - 'old text', - 10, - 10, - ); + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setText('old text'); const origEvent = new Blockly.Events.CommentChange( comment, 'old text', diff --git a/tests/mocha/event_comment_collapse_test.js b/tests/mocha/event_comment_collapse_test.js new file mode 100644 index 000000000..86b36b075 --- /dev/null +++ b/tests/mocha/event_comment_collapse_test.js @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Comment Collapse Event', function () { + setup(function () { + sharedTestSetup.call(this); + this.workspace = new Blockly.Workspace(); + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + suite('Serialization', function () { + test('events round-trip through JSON', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + const origEvent = new Blockly.Events.CommentCollapse(comment, true); + + const json = origEvent.toJson(); + const newEvent = new Blockly.Events.fromJson(json, this.workspace); + + chai.assert.deepEqual(newEvent, origEvent); + }); + }); +}); diff --git a/tests/mocha/event_comment_create_test.js b/tests/mocha/event_comment_create_test.js index d140eff85..57c246f1f 100644 --- a/tests/mocha/event_comment_create_test.js +++ b/tests/mocha/event_comment_create_test.js @@ -21,12 +21,9 @@ suite('Comment Create Event', function () { suite('Serialization', function () { test('events round-trip through JSON', function () { - const comment = new Blockly.WorkspaceComment( - this.workspace, - 'test text', - 10, - 10, - ); + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setText('test text'); + comment.moveTo(new Blockly.utils.Coordinate(10, 10)); const origEvent = new Blockly.Events.CommentCreate(comment); const json = origEvent.toJson(); diff --git a/tests/mocha/event_comment_delete_test.js b/tests/mocha/event_comment_delete_test.js index fc4b7701f..e0a8a98db 100644 --- a/tests/mocha/event_comment_delete_test.js +++ b/tests/mocha/event_comment_delete_test.js @@ -21,12 +21,9 @@ suite('Comment Delete Event', function () { suite('Serialization', function () { test('events round-trip through JSON', function () { - const comment = new Blockly.WorkspaceComment( - this.workspace, - 'test text', - 10, - 10, - ); + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setText('test text'); + comment.moveTo(new Blockly.utils.Coordinate(10, 10)); const origEvent = new Blockly.Events.CommentDelete(comment); const json = origEvent.toJson(); diff --git a/tests/mocha/event_comment_move_test.js b/tests/mocha/event_comment_move_test.js index a0b0b5eab..420bdbb52 100644 --- a/tests/mocha/event_comment_move_test.js +++ b/tests/mocha/event_comment_move_test.js @@ -21,14 +21,11 @@ suite('Comment Move Event', function () { suite('Serialization', function () { test('events round-trip through JSON', function () { - const comment = new Blockly.WorkspaceComment( - this.workspace, - 'test text', - 10, - 10, - ); + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setText('test text'); + comment.moveTo(new Blockly.utils.Coordinate(10, 10)); const origEvent = new Blockly.Events.CommentMove(comment); - comment.moveBy(10, 10); + comment.moveTo(new Blockly.utils.Coordinate(20, 20)); origEvent.recordNew(); const json = origEvent.toJson(); diff --git a/tests/mocha/event_test.js b/tests/mocha/event_test.js index e59cb4911..75688c654 100644 --- a/tests/mocha/event_test.js +++ b/tests/mocha/event_test.js @@ -19,7 +19,6 @@ import { workspaceTeardown, } from './test_helpers/setup_teardown.js'; import * as eventUtils from '../../build/src/core/events/utils.js'; -import {WorkspaceComment} from '../../build/src/core/workspace_comment.js'; suite('Events', function () { setup(function () { @@ -824,7 +823,19 @@ suite('Events', function () { type: 'comment_create', group: '', commentId: thisObj.comment.id, - xml: Blockly.Xml.domToText(thisObj.comment.toXmlWithXY()), + // TODO: Before merging, is this a dumb change detector? + xml: Blockly.Xml.domToText( + Blockly.Xml.saveWorkspaceComment(thisObj.comment), + {addCoordinates: true}, + ), + json: { + height: 100, + width: 120, + id: 'comment id', + x: 0, + y: 0, + text: 'test text', + }, }), }, { @@ -835,7 +846,19 @@ suite('Events', function () { type: 'comment_delete', group: '', commentId: thisObj.comment.id, - xml: Blockly.Xml.domToText(thisObj.comment.toXmlWithXY()), + // TODO: Before merging, is this a dumb change detector? + xml: Blockly.Xml.domToText( + Blockly.Xml.saveWorkspaceComment(thisObj.comment), + {addCoordinates: true}, + ), + json: { + height: 100, + width: 120, + id: 'comment id', + x: 0, + y: 0, + text: 'test text', + }, }), }, // TODO(#4577) Test serialization of move event coordinate properties. @@ -873,13 +896,11 @@ suite('Events', function () { title: 'WorkspaceComment events', testCases: workspaceCommentEventTestCases, setup: (thisObj) => { - thisObj.comment = new Blockly.WorkspaceComment( + thisObj.comment = new Blockly.comments.WorkspaceComment( thisObj.workspace, - 'comment text', - 0, - 0, 'comment id', ); + thisObj.comment.setText('test text'); }, }, ]; diff --git a/tests/mocha/field_angle_test.js b/tests/mocha/field_angle_test.js deleted file mode 100644 index d2630581f..000000000 --- a/tests/mocha/field_angle_test.js +++ /dev/null @@ -1,390 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as Blockly from '../../build/src/core/blockly.js'; -import { - assertFieldValue, - runConstructorSuiteTests, - runFromJsonSuiteTests, - runSetValueTests, -} from './test_helpers/fields.js'; -import { - createTestBlock, - defineRowBlock, -} from './test_helpers/block_definitions.js'; -import { - sharedTestSetup, - sharedTestTeardown, - workspaceTeardown, -} from './test_helpers/setup_teardown.js'; - -suite('Angle Fields', function () { - setup(function () { - sharedTestSetup.call(this); - }); - teardown(function () { - sharedTestTeardown.call(this); - }); - /** - * Configuration for field tests with invalid values. - * @type {!Array} - */ - const invalidValueTestCases = [ - {title: 'Undefined', value: undefined}, - {title: 'Null', value: null}, - {title: 'NaN', value: NaN}, - {title: 'Non-Parsable String', value: 'bad'}, - {title: 'Infinity', value: Infinity, expectedValue: Infinity}, - {title: 'Negative Infinity', value: -Infinity, expectedValue: -Infinity}, - {title: 'Infinity String', value: 'Infinity', expectedValue: Infinity}, - { - title: 'Negative Infinity String', - value: '-Infinity', - expectedValue: -Infinity, - }, - ]; - /** - * Configuration for field tests with valid values. - * @type {!Array} - */ - - const validValueTestCases = [ - {title: 'Integer', value: 1, expectedValue: 1}, - {title: 'Float', value: 1.5, expectedValue: 1.5}, - {title: 'Integer String', value: '1', expectedValue: 1}, - {title: 'Float String', value: '1.5', expectedValue: 1.5}, - {title: '> 360°', value: 362, expectedValue: 2}, - ]; - const addArgsAndJson = function (testCase) { - testCase.args = [testCase.value]; - testCase.json = {'angle': testCase.value}; - }; - invalidValueTestCases.forEach(addArgsAndJson); - validValueTestCases.forEach(addArgsAndJson); - - /** - * The expected default value for the field being tested. - * @type {*} - */ - const defaultFieldValue = 0; - /** - * Asserts that the field property values are set to default. - * @param {FieldTemplate} field The field to check. - */ - const assertFieldDefault = function (field) { - assertFieldValue(field, defaultFieldValue); - }; - /** - * Asserts that the field properties are correct based on the test case. - * @param {!Blockly.FieldAngle} field The field to check. - * @param {!FieldValueTestCase} testCase The test case. - */ - const validTestCaseAssertField = function (field, testCase) { - assertFieldValue(field, testCase.expectedValue); - }; - - runConstructorSuiteTests( - Blockly.FieldAngle, - validValueTestCases, - invalidValueTestCases, - validTestCaseAssertField, - assertFieldDefault, - ); - - runFromJsonSuiteTests( - Blockly.FieldAngle, - validValueTestCases, - invalidValueTestCases, - validTestCaseAssertField, - assertFieldDefault, - ); - - suite('setValue', function () { - suite('Empty -> New Value', function () { - setup(function () { - this.field = new Blockly.FieldAngle(); - }); - runSetValueTests( - validValueTestCases, - invalidValueTestCases, - defaultFieldValue, - ); - test('With source block', function () { - this.field.setSourceBlock(createTestBlock()); - this.field.setValue(2.5); - assertFieldValue(this.field, 2.5); - }); - }); - suite('Value -> New Value', function () { - const initialValue = 1; - setup(function () { - this.field = new Blockly.FieldAngle(initialValue); - }); - runSetValueTests( - validValueTestCases, - invalidValueTestCases, - initialValue, - ); - test('With source block', function () { - this.field.setSourceBlock(createTestBlock()); - this.field.setValue(2.5); - assertFieldValue(this.field, 2.5); - }); - }); - }); - suite('Validators', function () { - setup(function () { - this.field = new Blockly.FieldAngle(1); - this.field.valueWhenEditorWasOpened_ = this.field.getValue(); - this.field.htmlInput_ = document.createElement('input'); - this.field.htmlInput_.setAttribute('data-old-value', '1'); - this.field.htmlInput_.setAttribute('data-untyped-default-value', '1'); - this.stub = sinon.stub(this.field, 'resizeEditor_'); - }); - teardown(function () { - sinon.restore(); - }); - const testSuites = [ - { - title: 'Null Validator', - validator: function () { - return null; - }, - value: 2, - expectedValue: 1, - }, - { - title: 'Force Mult of 30 Validator', - validator: function (newValue) { - return Math.round(newValue / 30) * 30; - }, - value: 25, - expectedValue: 30, - }, - { - title: 'Returns Undefined Validator', - validator: function () {}, - value: 2, - expectedValue: 2, - }, - ]; - testSuites.forEach(function (suiteInfo) { - suite(suiteInfo.title, function () { - setup(function () { - this.field.setValidator(suiteInfo.validator); - }); - test('When Editing', function () { - this.field.isBeingEdited_ = true; - this.field.htmlInput_.value = String(suiteInfo.value); - this.field.onHtmlInputChange_(null); - assertFieldValue( - this.field, - suiteInfo.expectedValue, - String(suiteInfo.value), - ); - }); - test('When Not Editing', function () { - this.field.setValue(suiteInfo.value); - assertFieldValue(this.field, +suiteInfo.expectedValue); - }); - }); - }); - }); - suite('Customizations', function () { - suite('Clockwise', function () { - test('JS Configuration', function () { - const field = new Blockly.FieldAngle(0, null, { - clockwise: true, - }); - chai.assert.isTrue(field.clockwise); - }); - test('JSON Definition', function () { - const field = Blockly.FieldAngle.fromJson({ - value: 0, - clockwise: true, - }); - chai.assert.isTrue(field.clockwise); - }); - test('Constant', function () { - // Note: Generally constants should be set at compile time, not - // runtime (since they are constants) but for testing purposes we - // can do this. - Blockly.FieldAngle.CLOCKWISE = true; - const field = new Blockly.FieldAngle(); - chai.assert.isTrue(field.clockwise); - }); - }); - suite('Offset', function () { - test('JS Configuration', function () { - const field = new Blockly.FieldAngle(0, null, { - offset: 90, - }); - chai.assert.equal(field.offset, 90); - }); - test('JSON Definition', function () { - const field = Blockly.FieldAngle.fromJson({ - value: 0, - offset: 90, - }); - chai.assert.equal(field.offset, 90); - }); - test('Constant', function () { - // Note: Generally constants should be set at compile time, not - // runtime (since they are constants) but for testing purposes we - // can do this. - Blockly.FieldAngle.OFFSET = 90; - const field = new Blockly.FieldAngle(); - chai.assert.equal(field.offset, 90); - }); - test('Null', function () { - // Note: Generally constants should be set at compile time, not - // runtime (since they are constants) but for testing purposes we - // can do this. - Blockly.FieldAngle.OFFSET = 90; - const field = Blockly.FieldAngle.fromJson({ - value: 0, - offset: null, - }); - chai.assert.equal(field.offset, 90); - }); - }); - suite('Wrap', function () { - test('JS Configuration', function () { - const field = new Blockly.FieldAngle(0, null, { - wrap: 180, - }); - chai.assert.equal(field.wrap, 180); - }); - test('JSON Definition', function () { - const field = Blockly.FieldAngle.fromJson({ - value: 0, - wrap: 180, - }); - chai.assert.equal(field.wrap, 180); - }); - test('Constant', function () { - // Note: Generally constants should be set at compile time, not - // runtime (since they are constants) but for testing purposes we - // can do this. - Blockly.FieldAngle.WRAP = 180; - const field = new Blockly.FieldAngle(); - chai.assert.equal(field.wrap, 180); - }); - test('Null', function () { - // Note: Generally constants should be set at compile time, not - // runtime (since they are constants) but for testing purposes we - // can do this. - Blockly.FieldAngle.WRAP = 180; - const field = Blockly.FieldAngle.fromJson({ - value: 0, - wrap: null, - }); - chai.assert.equal(field.wrap, 180); - }); - }); - suite('Round', function () { - test('JS Configuration', function () { - const field = new Blockly.FieldAngle(0, null, { - round: 30, - }); - chai.assert.equal(field.round, 30); - }); - test('JSON Definition', function () { - const field = Blockly.FieldAngle.fromJson({ - value: 0, - round: 30, - }); - chai.assert.equal(field.round, 30); - }); - test('Constant', function () { - // Note: Generally constants should be set at compile time, not - // runtime (since they are constants) but for testing purposes we - // can do this. - Blockly.FieldAngle.ROUND = 30; - const field = new Blockly.FieldAngle(); - chai.assert.equal(field.round, 30); - }); - test('Null', function () { - // Note: Generally constants should be set at compile time, not - // runtime (since they are constants) but for testing purposes we - // can do this. - Blockly.FieldAngle.ROUND = 30; - const field = Blockly.FieldAngle.fromJson({ - value: 0, - round: null, - }); - chai.assert.equal(field.round, 30); - }); - }); - suite('Mode', function () { - suite('Compass', function () { - test('JS Configuration', function () { - const field = new Blockly.FieldAngle(0, null, { - mode: 'compass', - }); - chai.assert.equal(field.offset, 90); - chai.assert.isTrue(field.clockwise); - }); - test('JS Configuration', function () { - const field = Blockly.FieldAngle.fromJson({ - value: 0, - mode: 'compass', - }); - chai.assert.equal(field.offset, 90); - chai.assert.isTrue(field.clockwise); - }); - }); - suite('Protractor', function () { - test('JS Configuration', function () { - const field = new Blockly.FieldAngle(0, null, { - mode: 'protractor', - }); - chai.assert.equal(field.offset, 0); - chai.assert.isFalse(field.clockwise); - }); - test('JS Configuration', function () { - const field = Blockly.FieldAngle.fromJson({ - value: 0, - mode: 'protractor', - }); - chai.assert.equal(field.offset, 0); - chai.assert.isFalse(field.clockwise); - }); - }); - }); - }); - - suite('Serialization', function () { - setup(function () { - this.workspace = new Blockly.Workspace(); - defineRowBlock(); - - this.assertValue = (value) => { - const block = this.workspace.newBlock('row_block'); - const field = new Blockly.FieldAngle(value); - block.getInput('INPUT').appendField(field, 'ANGLE'); - const jso = Blockly.serialization.blocks.save(block); - chai.assert.deepEqual(jso['fields'], {'ANGLE': value}); - }; - }); - - teardown(function () { - workspaceTeardown.call(this, this.workspace); - }); - - test('Simple', function () { - this.assertValue(90); - }); - - test('Max precision', function () { - this.assertValue(1.000000000000001); - }); - - test('Smallest number', function () { - this.assertValue(5e-324); - }); - }); -}); diff --git a/tests/mocha/field_multilineinput_test.js b/tests/mocha/field_multilineinput_test.js deleted file mode 100644 index abdf68237..000000000 --- a/tests/mocha/field_multilineinput_test.js +++ /dev/null @@ -1,284 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as Blockly from '../../build/src/core/blockly.js'; -import { - assertFieldValue, - runConstructorSuiteTests, - runFromJsonSuiteTests, - runSetValueTests, -} from './test_helpers/fields.js'; -import { - createTestBlock, - defineRowBlock, -} from './test_helpers/block_definitions.js'; -import { - sharedTestSetup, - sharedTestTeardown, - workspaceTeardown, -} from './test_helpers/setup_teardown.js'; -import {runCodeGenerationTestSuites} from './test_helpers/code_generation.js'; -import {dartGenerator} from '../../build/src/generators/dart.js'; -import {javascriptGenerator} from '../../build/src/generators/javascript.js'; -import {luaGenerator} from '../../build/src/generators/lua.js'; -import {phpGenerator} from '../../build/src/generators/php.js'; -import {pythonGenerator} from '../../build/src/generators/python.js'; - -suite('Multiline Input Fields', function () { - setup(function () { - sharedTestSetup.call(this); - }); - teardown(function () { - sharedTestTeardown.call(this); - }); - /** - * Configuration for field tests with invalid values. - * @type {!Array} - */ - const invalidValueTestCases = [ - {title: 'Undefined', value: undefined}, - {title: 'Null', value: null}, - ]; - /** - * Configuration for field tests with valid values. - * @type {!Array} - */ - const validValueTestCases = [ - {title: 'Empty string', value: '', expectedValue: ''}, - {title: 'String no newline', value: 'value', expectedValue: 'value'}, - { - title: 'String with newline', - value: 'bark bark\n bark bark bark\n bark bar bark bark\n', - expectedValue: 'bark bark\n bark bark bark\n bark bar bark bark\n', - }, - {title: 'Boolean true', value: true, expectedValue: 'true'}, - {title: 'Boolean false', value: false, expectedValue: 'false'}, - {title: 'Number (Truthy)', value: 1, expectedValue: '1'}, - {title: 'Number (Falsy)', value: 0, expectedValue: '0'}, - {title: 'NaN', value: NaN, expectedValue: 'NaN'}, - ]; - const addArgsAndJson = function (testCase) { - testCase.args = [testCase.value]; - testCase.json = {'text': testCase.value}; - }; - invalidValueTestCases.forEach(addArgsAndJson); - validValueTestCases.forEach(addArgsAndJson); - - /** - * The expected default value for the field being tested. - * @type {*} - */ - const defaultFieldValue = ''; - /** - * Asserts that the field property values are set to default. - * @param {!Blockly.FieldMultilineInput} field The field to check. - */ - const assertFieldDefault = function (field) { - assertFieldValue(field, defaultFieldValue); - }; - /** - * Asserts that the field properties are correct based on the test case. - * @param {!Blockly.FieldMultilineInput} field The field to check. - * @param {!FieldValueTestCase} testCase The test case. - */ - const validTestCaseAssertField = function (field, testCase) { - assertFieldValue(field, testCase.expectedValue); - }; - - runConstructorSuiteTests( - Blockly.FieldMultilineInput, - validValueTestCases, - invalidValueTestCases, - validTestCaseAssertField, - assertFieldDefault, - ); - - runFromJsonSuiteTests( - Blockly.FieldMultilineInput, - validValueTestCases, - invalidValueTestCases, - validTestCaseAssertField, - assertFieldDefault, - ); - - suite('setValue', function () { - suite('Empty -> New Value', function () { - setup(function () { - this.field = new Blockly.FieldMultilineInput(); - }); - runSetValueTests( - validValueTestCases, - invalidValueTestCases, - defaultFieldValue, - ); - test('With source block', function () { - this.field.setSourceBlock(createTestBlock()); - this.field.setValue('value'); - assertFieldValue(this.field, 'value'); - }); - }); - suite('Value -> New Value', function () { - const initialValue = 'oldValue'; - setup(function () { - this.field = new Blockly.FieldMultilineInput(initialValue); - }); - runSetValueTests( - validValueTestCases, - invalidValueTestCases, - initialValue, - ); - test('With source block', function () { - this.field.setSourceBlock(createTestBlock()); - this.field.setValue('value'); - assertFieldValue(this.field, 'value'); - }); - }); - }); - - suite('blockToCode', function () { - setup(function () { - this.workspace = new Blockly.Workspace(); - }); - const createBlockFn = (value) => { - return (workspace) => { - const block = workspace.newBlock('text_multiline'); - const textField = block.getField('TEXT'); - textField.setValue(value); - return block; - }; - }; - - /** - * Test suites for code generation tests.s - * @type {Array} - */ - const testSuites = [ - { - title: 'Dart', - generator: dartGenerator, - testCases: [ - { - title: 'Empty string', - expectedCode: "''", - createBlock: createBlockFn(''), - }, - { - title: 'String with newline', - expectedCode: - "'bark bark' + '\\n' + \n' bark bark bark' + '\\n' + \n' bark bar bark bark' + '\\n' + \n''", - createBlock: createBlockFn( - 'bark bark\n bark bark bark\n bark bar bark bark\n', - ), - }, - ], - }, - { - title: 'JavaScript', - generator: javascriptGenerator, - testCases: [ - { - title: 'Empty string', - expectedCode: "''", - createBlock: createBlockFn(''), - }, - { - title: 'String with newline', - expectedCode: - "'bark bark' + '\\n' +\n' bark bark bark' + '\\n' +\n' bark bar bark bark' + '\\n' +\n''", - createBlock: createBlockFn( - 'bark bark\n bark bark bark\n bark bar bark bark\n', - ), - }, - ], - }, - { - title: 'Lua', - generator: luaGenerator, - testCases: [ - { - title: 'Empty string', - expectedCode: "''", - createBlock: createBlockFn(''), - }, - { - title: 'String with newline', - expectedCode: - "'bark bark' .. '\\n' ..\n' bark bark bark' .. '\\n' ..\n' bark bar bark bark' .. '\\n' ..\n''", - createBlock: createBlockFn( - 'bark bark\n bark bark bark\n bark bar bark bark\n', - ), - }, - ], - }, - { - title: 'PHP', - generator: phpGenerator, - testCases: [ - { - title: 'Empty string', - expectedCode: "''", - createBlock: createBlockFn(''), - }, - { - title: 'String with newline', - expectedCode: - "'bark bark' . \"\\n\" .\n' bark bark bark' . \"\\n\" .\n' bark bar bark bark' . \"\\n\" .\n''", - createBlock: createBlockFn( - 'bark bark\n bark bark bark\n bark bar bark bark\n', - ), - }, - ], - }, - { - title: 'Python', - generator: pythonGenerator, - testCases: [ - { - title: 'Empty string', - expectedCode: "''", - createBlock: createBlockFn(''), - }, - { - title: 'String with newline', - expectedCode: - "'bark bark' + '\\n' + \n' bark bark bark' + '\\n' + \n' bark bar bark bark' + '\\n' + \n''", - createBlock: createBlockFn( - 'bark bark\n bark bark bark\n bark bar bark bark\n', - ), - }, - ], - }, - ]; - runCodeGenerationTestSuites(testSuites); - }); - - suite('Serialization', function () { - setup(function () { - this.workspace = new Blockly.Workspace(); - defineRowBlock(); - - this.assertValue = (value) => { - const block = this.workspace.newBlock('row_block'); - const field = new Blockly.FieldMultilineInput(value); - block.getInput('INPUT').appendField(field, 'MULTILINE'); - const jso = Blockly.serialization.blocks.save(block); - chai.assert.deepEqual(jso['fields'], {'MULTILINE': value}); - }; - }); - - teardown(function () { - workspaceTeardown.call(this, this.workspace); - }); - - test('Single line', function () { - this.assertValue('this is a single line'); - }); - - test('Multiple lines', function () { - this.assertValue('this\nis\n multiple\n lines'); - }); - }); -}); diff --git a/tests/mocha/generator_test.js b/tests/mocha/generator_test.js index 9ac67b27c..3a2679dca 100644 --- a/tests/mocha/generator_test.js +++ b/tests/mocha/generator_test.js @@ -92,7 +92,7 @@ suite('Generator', function () { return 'stack_block'; }; rowBlock.nextConnection.connect(stackBlock.previousConnection); - rowBlock.disabled = blockDisabled; + rowBlock.setDisabledReason(blockDisabled, 'test reason'); const code = generator.blockToCode(rowBlock, opt_thisOnly); delete generator.forBlock['stack_block']; @@ -115,11 +115,16 @@ suite('Generator', function () { const name = testCase[1]; test(name, function () { generator.init(this.workspace); - this.blockToCodeTest(generator, false, true, 'row_block'); this.blockToCodeTest( generator, - false, - false, + /* blockDisabled = */ false, + /* opt_thisOnly = */ true, + 'row_block', + ); + this.blockToCodeTest( + generator, + /* blockDisabled = */ false, + /* opt_thisOnly = */ false, 'row_blockstack_block', 'thisOnly=false', ); @@ -132,11 +137,16 @@ suite('Generator', function () { const generator = testCase[0]; const name = testCase[1]; test(name, function () { - this.blockToCodeTest(generator, true, true, ''); this.blockToCodeTest( generator, - true, - false, + /* blockDisabled = */ true, + /* opt_thisOnly = */ true, + '', + ); + this.blockToCodeTest( + generator, + /* blockDisabled = */ true, + /* opt_thisOnly = */ false, 'stack_block', 'thisOnly=false', ); diff --git a/tests/mocha/icon_test.js b/tests/mocha/icon_test.js index 4ff27997f..3463d8ad8 100644 --- a/tests/mocha/icon_test.js +++ b/tests/mocha/icon_test.js @@ -68,24 +68,6 @@ suite('Icon', function () { ); }); - test('initView is called by headful blocks during initSvg', function () { - const workspace = createWorkspaceSvg(); - const block = createUninitializedBlock(workspace); - const icon = new MockIcon(); - const initViewSpy = sinon.spy(icon, 'initView'); - - block.addIcon(icon); - chai.assert.isFalse( - initViewSpy.called, - 'Expected initView to not be called before initing svg', - ); - block.initSvg(); - chai.assert.isTrue( - initViewSpy.calledOnce, - 'Expected initView to be called', - ); - }); - test( 'initView is called by headful blocks that are currently ' + 'rendered when the icon is added', @@ -120,24 +102,6 @@ suite('Icon', function () { ); }); - test('applyColour is called by headful blocks during initSvg', function () { - const workspace = createWorkspaceSvg(); - const block = createUninitializedBlock(workspace); - const icon = new MockIcon(); - const applyColourSpy = sinon.spy(icon, 'applyColour'); - - block.addIcon(icon); - chai.assert.isFalse( - applyColourSpy.called, - 'Expected applyCOlour to not be called before initing svg', - ); - block.initSvg(); - chai.assert.isTrue( - applyColourSpy.calledOnce, - 'Expected applyColour to be called', - ); - }); - test( 'applyColour is called by headful blocks that are currently ' + 'rendered when the icon is added', @@ -193,7 +157,7 @@ suite('Icon', function () { block.addIcon(icon); applyColourSpy.resetHistory(); - block.setEnabled(false); + block.setDisabledReason(true, 'test reason'); chai.assert.isTrue( applyColourSpy.calledOnce, 'Expected applyColour to be called', @@ -231,24 +195,6 @@ suite('Icon', function () { ); }); - test('updateEditable is called by headful blocks during initSvg', function () { - const workspace = createWorkspaceSvg(); - const block = createUninitializedBlock(workspace); - const icon = new MockIcon(); - const updateEditableSpy = sinon.spy(icon, 'updateEditable'); - - block.addIcon(icon); - chai.assert.isFalse( - updateEditableSpy.called, - 'Expected updateEditable to not be called before initing svg', - ); - block.initSvg(); - chai.assert.isTrue( - updateEditableSpy.calledOnce, - 'Expected updateEditable to be called', - ); - }); - test( 'updateEditable is called by headful blocks that are currently ' + 'rendered when the icon is added', diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 6c4e5ad0c..9c7b10cab 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -63,6 +63,7 @@ import './event_bubble_open_test.js'; import './event_click_test.js'; import './event_comment_change_test.js'; + import './event_comment_collapse_test.js'; import './event_comment_create_test.js'; import './event_comment_delete_test.js'; import './event_comment_move_test.js'; @@ -76,14 +77,11 @@ import './event_var_rename_test.js'; import './event_viewport_test.js'; import './extensions_test.js'; - import './field_angle_test.js'; import './field_checkbox_test.js'; - import './field_colour_test.js'; import './field_dropdown_test.js'; import './field_image_test.js'; import './field_label_serializable_test.js'; import './field_label_test.js'; - import './field_multilineinput_test.js'; import './field_number_test.js'; import './field_registry_test.js'; import './field_test.js'; @@ -103,9 +101,12 @@ import './layering_test.js'; import './blocks/lists_test.js'; import './blocks/logic_ternary_test.js'; + import './blocks/loops_test.js'; import './metrics_test.js'; import './mutator_test.js'; import './names_test.js'; + // TODO: Remove these tests. + import './old_workspace_comment_test.js'; import './procedure_map_test.js'; import './blocks/procedures_test.js'; import './registry_test.js'; @@ -122,6 +123,7 @@ import './variable_model_test.js'; import './blocks/variables_test.js'; import './widget_div_test.js'; + import './comment_view_test.js'; import './workspace_comment_test.js'; import './workspace_svg_test.js'; import './workspace_test.js'; diff --git a/tests/mocha/input_test.js b/tests/mocha/input_test.js index c4ecd7a39..73baf20f6 100644 --- a/tests/mocha/input_test.js +++ b/tests/mocha/input_test.js @@ -27,14 +27,12 @@ suite('Inputs', function () { ); this.renderStub = sinon.stub(this.block, 'queueRender'); - this.bumpNeighboursStub = sinon.stub(this.block, 'bumpNeighbours'); this.dummy = this.block.appendDummyInput('DUMMY'); this.value = this.block.appendValueInput('VALUE'); this.statement = this.block.appendStatementInput('STATEMENT'); this.renderStub.resetHistory(); - this.bumpNeighboursStub.resetHistory(); }); teardown(function () { sharedTestTeardown.call(this); @@ -158,7 +156,6 @@ suite('Inputs', function () { chai.assert.equal(setBlockSpy.getCall(0).args[0], this.block); sinon.assert.calledOnce(initSpy); sinon.assert.calledOnce(this.renderStub); - sinon.assert.calledOnce(this.bumpNeighboursStub); setBlockSpy.restore(); initSpy.restore(); @@ -177,7 +174,6 @@ suite('Inputs', function () { chai.assert.equal(setBlockSpy.getCall(0).args[0], this.block); sinon.assert.calledOnce(initModelSpy); sinon.assert.notCalled(this.renderStub); - sinon.assert.notCalled(this.bumpNeighboursStub); setBlockSpy.restore(); initModelSpy.restore(); @@ -196,12 +192,10 @@ suite('Inputs', function () { this.dummy.appendField(field, 'FIELD'); this.renderStub.resetHistory(); - this.bumpNeighboursStub.resetHistory(); this.dummy.removeField('FIELD'); sinon.assert.calledOnce(disposeSpy); sinon.assert.calledOnce(this.renderStub); - sinon.assert.calledOnce(this.bumpNeighboursStub); }); test('Headless', function () { const field = new Blockly.FieldLabel('field'); @@ -209,14 +203,12 @@ suite('Inputs', function () { this.dummy.appendField(field, 'FIELD'); this.renderStub.resetHistory(); - this.bumpNeighboursStub.resetHistory(); this.block.rendered = false; this.dummy.removeField('FIELD'); sinon.assert.calledOnce(disposeSpy); sinon.assert.notCalled(this.renderStub); - sinon.assert.notCalled(this.bumpNeighboursStub); }); }); suite('Field Ordering/Manipulation', function () { diff --git a/tests/mocha/jso_deserialization_test.js b/tests/mocha/jso_deserialization_test.js index d58e208fb..7c8a06db8 100644 --- a/tests/mocha/jso_deserialization_test.js +++ b/tests/mocha/jso_deserialization_test.js @@ -11,14 +11,20 @@ import { } from './test_helpers/setup_teardown.js'; import {assertEventFired} from './test_helpers/events.js'; import * as eventUtils from '../../build/src/core/events/utils.js'; +import { + MockParameterModel, + MockProcedureModel, +} from './test_helpers/procedures.js'; suite('JSO Deserialization', function () { setup(function () { sharedTestSetup.call(this); + this.sandbox = sinon.createSandbox(); this.workspace = new Blockly.Workspace(); }); teardown(function () { + this.sandbox.restore(); sharedTestTeardown.call(this); }); @@ -785,95 +791,6 @@ suite('JSO Deserialization', function () { }); suite('Procedures', function () { - class MockProcedureModel { - constructor(workspace, name, id) { - this.id = id ?? Blockly.utils.idGenerator.genUid(); - this.name = name; - this.parameters = []; - this.returnTypes = null; - this.enabled = true; - } - - setName(name) { - this.name = name; - return this; - } - - insertParameter(parameterModel, index) { - this.parameters.splice(index, 0, parameterModel); - return this; - } - - deleteParameter(index) { - this.parameters.splice(index, 1); - return this; - } - - setReturnTypes(types) { - this.returnTypes = types; - return this; - } - - setEnabled(enabled) { - this.enabled = enabled; - return this; - } - - getId() { - return this.id; - } - - getName() { - return this.name; - } - - getParameter(index) { - return this.parameters[index]; - } - - getParameters() { - return [...this.parameters]; - } - - getReturnTypes() { - return this.returnTypes; - } - - getEnabled() { - return this.enabled; - } - } - - class MockParameterModel { - constructor(workspace, name, id) { - this.id = id ?? Blockly.utils.idGenerator.genUid(); - this.name = name; - this.types = []; - } - - setName(name) { - this.name = name; - return this; - } - - setTypes(types) { - this.types = types; - return this; - } - - getName() { - return this.name; - } - - getTypes() { - return this.types; - } - - getId() { - return this.id; - } - } - setup(function () { this.procedureSerializer = new Blockly.serialization.procedures.ProcedureSerializer( @@ -888,232 +805,46 @@ suite('JSO Deserialization', function () { this.procedureMap = null; }); - suite('invariant properties', function () { - test('the id property is assigned', function () { - const jso = { - 'id': 'test id', - 'name': 'test name', - 'returnTypes': [], - }; + test('load is called for the procedure model', function () { + const state = [ + { + 'id': 'test', + 'parameters': [], + }, + ]; + const spy = this.sandbox.spy(MockProcedureModel, 'loadState'); - this.procedureSerializer.load([jso], this.workspace); + this.procedureSerializer.load(state, this.workspace); - const procedureModel = this.procedureMap.getProcedures()[0]; - chai.assert.isNotNull( - procedureModel, - 'Expected a procedure model to exist', - ); - chai.assert.equal( - procedureModel.getId(), - 'test id', - 'Expected the procedure model ID to match the serialized ID', - ); - }); - - test('the name property is assigned', function () { - const jso = { - 'id': 'test id', - 'name': 'test name', - 'returnTypes': [], - }; - - this.procedureSerializer.load([jso], this.workspace); - - const procedureModel = this.procedureMap.getProcedures()[0]; - chai.assert.isNotNull( - procedureModel, - 'Expected a procedure model to exist', - ); - chai.assert.equal( - procedureModel.getName(), - 'test name', - 'Expected the procedure model name to match the serialized name', - ); - }); + chai.assert.isTrue( + spy.calledOnce, + 'Expected the loadState method to be called', + ); }); - suite('return types', function () { - test('if the return type property is null it is assigned', function () { - const jso = { - 'id': 'test id', - 'name': 'test name', - 'returnTypes': null, - }; + test('load is called for each parameter model', function () { + const state = [ + { + 'id': 'test', + 'parameters': [ + { + 'id': 'test1', + }, + { + 'id': 'test2', + }, + ], + }, + ]; - this.procedureSerializer.load([jso], this.workspace); + const spy = this.sandbox.spy(MockParameterModel, 'loadState'); - const procedureModel = this.procedureMap.getProcedures()[0]; - chai.assert.isNotNull( - procedureModel, - 'Expected a procedure model to exist', - ); - chai.assert.isNull( - procedureModel.getReturnTypes(), - 'Expected the procedure model types to be null', - ); - }); + this.procedureSerializer.load(state, this.workspace); - test('if the return type property is an empty array it is assigned', function () { - const jso = { - 'id': 'test id', - 'name': 'test name', - 'returnTypes': [], - }; - - this.procedureSerializer.load([jso], this.workspace); - - const procedureModel = this.procedureMap.getProcedures()[0]; - chai.assert.isNotNull( - procedureModel, - 'Expected a procedure model to exist', - ); - chai.assert.isArray( - procedureModel.getReturnTypes(), - 'Expected the procedure model types to be an array', - ); - chai.assert.isEmpty( - procedureModel.getReturnTypes(), - 'Expected the procedure model types array to be empty', - ); - }); - - test('if the return type property is a string array it is assigned', function () { - const jso = { - 'id': 'test id', - 'name': 'test name', - 'returnTypes': ['test type 1', 'test type 2'], - }; - - this.procedureSerializer.load([jso], this.workspace); - - const procedureModel = this.procedureMap.getProcedures()[0]; - chai.assert.isNotNull( - procedureModel, - 'Expected a procedure model to exist', - ); - chai.assert.isArray( - procedureModel.getReturnTypes(), - 'Expected the procedure model types to be an array', - ); - chai.assert.deepEqual( - procedureModel.getReturnTypes(), - ['test type 1', 'test type 2'], - 'Expected the procedure model types array to be match the ' + - 'serialized array', - ); - }); - }); - - suite('parameters', function () { - suite('invariant properties', function () { - test('the id property is assigned', function () { - const jso = { - 'id': 'test id', - 'name': 'test name', - 'returnTypes': [], - 'parameters': [ - { - 'id': 'test id', - 'name': 'test name', - }, - ], - }; - - this.procedureSerializer.load([jso], this.workspace); - - const parameterModel = this.procedureMap - .getProcedures()[0] - .getParameters()[0]; - chai.assert.isNotNull( - parameterModel, - 'Expected a parameter model to exist', - ); - chai.assert.equal( - parameterModel.getId(), - 'test id', - 'Expected the parameter model ID to match the serialized ID', - ); - }); - - test('the name property is assigned', function () { - const jso = { - 'id': 'test id', - 'name': 'test name', - 'returnTypes': [], - 'parameters': [ - { - 'id': 'test id', - 'name': 'test name', - }, - ], - }; - - this.procedureSerializer.load([jso], this.workspace); - - const parameterModel = this.procedureMap - .getProcedures()[0] - .getParameters()[0]; - chai.assert.isNotNull( - parameterModel, - 'Expected a parameter model to exist', - ); - chai.assert.equal( - parameterModel.getName(), - 'test name', - 'Expected the parameter model name to match the serialized name', - ); - }); - }); - - suite('types', function () { - test('if the type property does not exist, nothing is assigned', function () { - const jso = { - 'id': 'test id', - 'name': 'test name', - 'returnTypes': [], - 'parameters': [ - { - 'id': 'test id', - 'name': 'test name', - }, - ], - }; - - chai.assert.doesNotThrow(() => { - this.procedureMap.getProcedures()[0].getParameters()[0]; - }, 'Expected the deserializer to skip the non-existant type property'); - }); - - test('if the type property exists, it is assigned', function () { - const jso = { - 'id': 'test id', - 'name': 'test name', - 'returnTypes': [], - 'parameters': [ - { - 'id': 'test id', - 'name': 'test name', - 'types': ['test type 1', 'test type 2'], - }, - ], - }; - - this.procedureSerializer.load([jso], this.workspace); - - const parameterModel = this.procedureMap - .getProcedures()[0] - .getParameters()[0]; - chai.assert.isNotNull( - parameterModel, - 'Expected a parameter model to exist', - ); - chai.assert.deepEqual( - parameterModel.getTypes(), - ['test type 1', 'test type 2'], - 'Expected the parameter model types to match the serialized types', - ); - }); - }); + chai.assert.isTrue( + spy.calledTwice, + 'Expected the loadState method to be called once for each parameter', + ); }); }); }); diff --git a/tests/mocha/jso_serialization_test.js b/tests/mocha/jso_serialization_test.js index 895ff0be2..72a74ad3d 100644 --- a/tests/mocha/jso_serialization_test.js +++ b/tests/mocha/jso_serialization_test.js @@ -25,6 +25,7 @@ suite('JSO Serialization', function () { setup(function () { sharedTestSetup.call(this); this.workspace = new Blockly.Workspace(); + this.sandbox = sinon.createSandbox(); defineStackBlock(); defineRowBlock(); @@ -34,6 +35,7 @@ suite('JSO Serialization', function () { }); teardown(function () { + this.sandbox.restore(); workspaceTeardown.call(this, this.workspace); sharedTestTeardown.call(this); }); @@ -84,19 +86,30 @@ suite('JSO Serialization', function () { }); }); - suite('Enabled', function () { - test('False', function () { + suite('DisabledReasons', function () { + test('One reason', function () { const block = this.workspace.newBlock('row_block'); - block.setEnabled(false); + block.setDisabledReason(true, 'test reason'); const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'enabled', false); + assertProperty(jso, 'disabledReasons', ['test reason']); }); - test('True', function () { + test('Zero reasons', function () { const block = this.workspace.newBlock('row_block'); - block.setEnabled(true); + block.setDisabledReason(false, 'test reason'); const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'enabled'); + assertNoProperty(jso, 'disabledReasons'); + }); + + test('Multiple reasons', function () { + const block = this.workspace.newBlock('row_block'); + block.setDisabledReason(true, 'test reason 1'); + block.setDisabledReason(true, 'test reason 2'); + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'disabledReasons', [ + 'test reason 1', + 'test reason 2', + ]); }); }); @@ -857,105 +870,200 @@ suite('JSO Serialization', function () { this.serializer = null; }); - suite('invariant properties', function () { - test('the state always has an id property', function () { - const procedureModel = new MockProcedureModel(); - this.procedureMap.add(procedureModel); - const jso = this.serializer.save(this.workspace); - const procedure = jso[0]; - assertProperty(procedure, 'id', procedureModel.getId()); + test('save is called on the procedure model', function () { + const proc = new MockProcedureModel(); + this.workspace.getProcedureMap().set('test', proc); + const spy = this.sandbox.spy(proc, 'saveState'); + + this.serializer.save(this.workspace); + + chai.assert.isTrue( + spy.calledOnce, + 'Expected the saveState method to be called on the procedure model', + ); + }); + + test('save is called on each parameter model', function () { + const proc = new MockProcedureModel(); + const param1 = new MockParameterModel(); + const param2 = new MockParameterModel(); + proc.insertParameter(param1, 0); + proc.insertParameter(param2, 1); + this.workspace.getProcedureMap().set('test', proc); + const spy1 = this.sandbox.spy(param1, 'saveState'); + const spy2 = this.sandbox.spy(param2, 'saveState'); + + this.serializer.save(this.workspace); + + chai.assert.isTrue( + spy1.calledOnce, + 'Expected the saveState method to be called on the first parameter model', + ); + chai.assert.isTrue( + spy2.calledOnce, + 'Expected the saveState method to be called on the first parameter model', + ); + }); + }); + + suite('Workspace comments', function () { + suite('IDs', function () { + test('IDs are saved by default', function () { + const comment = new Blockly.comments.WorkspaceComment( + this.workspace, + 'testID', + ); + + const json = Blockly.serialization.workspaceComments.save(comment); + + assertProperty(json, 'id', 'testID'); }); - test('if the name has not been set, name is an empty string', function () { - const procedureModel = new MockProcedureModel(); - this.procedureMap.add(procedureModel); - const jso = this.serializer.save(this.workspace); - const procedure = jso[0]; - assertProperty(procedure, 'name', ''); - }); + test('saving IDs can be disabled', function () { + const comment = new Blockly.comments.WorkspaceComment( + this.workspace, + 'testID', + ); - test('if the name has been set, name is the string', function () { - const procedureModel = new MockProcedureModel().setName('testName'); - this.procedureMap.add(procedureModel); - const jso = this.serializer.save(this.workspace); - const procedure = jso[0]; - assertProperty(procedure, 'name', 'testName'); + const json = Blockly.serialization.workspaceComments.save(comment, { + saveIds: false, + }); + + assertNoProperty(json, 'id'); }); }); - suite('return types', function () { - test('if the procedure does not return, returnTypes is null', function () { - const procedureModel = new MockProcedureModel(); - this.procedureMap.add(procedureModel); - const jso = this.serializer.save(this.workspace); - const procedure = jso[0]; - assertProperty(procedure, 'returnTypes', null); + suite('Coordinates', function () { + test('coordinates are not saved by default', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.moveTo(new Blockly.utils.Coordinate(42, 1337)); + + const json = Blockly.serialization.workspaceComments.save(comment); + + assertNoProperty(json, 'x'); + assertNoProperty(json, 'y'); }); - test('if the procedure has no return type, returnTypes is an empty array', function () { - const procedureModel = new MockProcedureModel().setReturnTypes([]); - this.procedureMap.add(procedureModel); - const jso = this.serializer.save(this.workspace); - const procedure = jso[0]; - assertProperty(procedure, 'returnTypes', []); - }); + test('saving coordinates can be enabled', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.moveTo(new Blockly.utils.Coordinate(42, 1337)); - test('if the procedure has return types, returnTypes is the array', function () { - const procedureModel = new MockProcedureModel().setReturnTypes([ - 'a type', - ]); - this.procedureMap.add(procedureModel); - const jso = this.serializer.save(this.workspace); - const procedure = jso[0]; - assertProperty(procedure, 'returnTypes', ['a type']); + const json = Blockly.serialization.workspaceComments.save(comment, { + addCoordinates: true, + }); + + assertProperty(json, 'x', 42); + assertProperty(json, 'y', 1337); }); }); - suite('parameters', function () { - suite('invariant properties', function () { - test('the state always has an id property', function () { - const parameterModel = new MockParameterModel('testparam'); - this.procedureMap.add( - new MockProcedureModel().insertParameter(parameterModel, 0), - ); - const jso = this.serializer.save(this.workspace); - const parameter = jso[0]['parameters'][0]; - assertProperty(parameter, 'id', parameterModel.getId()); - }); + suite('Text', function () { + test('the empty string is not saved', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setText(''); - test('the state always has a name property', function () { - const parameterModel = new MockParameterModel('testparam'); - this.procedureMap.add( - new MockProcedureModel().insertParameter(parameterModel, 0), - ); - const jso = this.serializer.save(this.workspace); - const parameter = jso[0]['parameters'][0]; - assertProperty(parameter, 'name', 'testparam'); - }); + const json = Blockly.serialization.workspaceComments.save(comment); + + assertNoProperty(json, 'text'); }); - suite('types', function () { - test('if the parameter has no type, there is no type property', function () { - const parameterModel = new MockParameterModel('testparam'); - this.procedureMap.add( - new MockProcedureModel().insertParameter(parameterModel, 0), - ); - const jso = this.serializer.save(this.workspace); - const parameter = jso[0]['parameters'][0]; - assertNoProperty(parameter, 'types'); - }); + test('text is saved', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setText('test text'); - test('if the parameter has types, types is an array', function () { - const parameterModel = new MockParameterModel('testparam').setTypes([ - 'a type', - ]); - this.procedureMap.add( - new MockProcedureModel().insertParameter(parameterModel, 0), - ); - const jso = this.serializer.save(this.workspace); - const parameter = jso[0]['parameters'][0]; - assertProperty(parameter, 'types', ['a type']); - }); + const json = Blockly.serialization.workspaceComments.save(comment); + + assertProperty(json, 'text', 'test text'); + }); + }); + + test('size is saved', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setSize(new Blockly.utils.Size(42, 1337)); + + const json = Blockly.serialization.workspaceComments.save(comment); + + assertProperty(json, 'width', 42); + assertProperty(json, 'height', 1337); + }); + + suite('Collapsed', function () { + test('collapsed is not saved if false', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setCollapsed(false); + + const json = Blockly.serialization.workspaceComments.save(comment); + + assertNoProperty(json, 'collapsed'); + }); + + test('collapsed is saved if true', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setCollapsed(true); + + const json = Blockly.serialization.workspaceComments.save(comment); + + assertProperty(json, 'collapsed', true); + }); + }); + + suite('Editable', function () { + test('editable is not saved if true', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setEditable(true); + + const json = Blockly.serialization.workspaceComments.save(comment); + + assertNoProperty(json, 'editable'); + }); + + test('editable is saved if false', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setEditable(false); + + const json = Blockly.serialization.workspaceComments.save(comment); + + assertProperty(json, 'editable', false); + }); + }); + + suite('Movable', function () { + test('movable is not saved if true', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setMovable(true); + + const json = Blockly.serialization.workspaceComments.save(comment); + + assertNoProperty(json, 'movable'); + }); + + test('movable is saved if false', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setMovable(false); + + const json = Blockly.serialization.workspaceComments.save(comment); + + assertProperty(json, 'movable', false); + }); + }); + + suite('Deletable', function () { + test('deletable is not saved if true', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setDeletable(true); + + const json = Blockly.serialization.workspaceComments.save(comment); + + assertNoProperty(json, 'deletable'); + }); + + test('deletable is saved if false', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setDeletable(false); + + const json = Blockly.serialization.workspaceComments.save(comment); + + assertProperty(json, 'deletable', false); }); }); }); diff --git a/tests/mocha/mutator_test.js b/tests/mocha/mutator_test.js index 609dc03a2..b4c6930fa 100644 --- a/tests/mocha/mutator_test.js +++ b/tests/mocha/mutator_test.js @@ -31,10 +31,10 @@ suite('Mutator', function () { sharedTestTeardown.call(this); }); - test('No change', function () { + test('No change', async function () { const block = createRenderedBlock(this.workspace, 'xml_block'); const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE); - icon.setBubbleVisible(true); + await icon.setBubbleVisible(true); const mutatorWorkspace = icon.getWorkspace(); // Trigger mutator change listener. createRenderedBlock(mutatorWorkspace, 'checkbox_block'); @@ -43,10 +43,10 @@ suite('Mutator', function () { }); }); - test('XML', function () { + test('XML', async function () { const block = createRenderedBlock(this.workspace, 'xml_block'); const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE); - icon.setBubbleVisible(true); + await icon.setBubbleVisible(true); const mutatorWorkspace = icon.getWorkspace(); mutatorWorkspace .getBlockById('check_block') @@ -63,10 +63,10 @@ suite('Mutator', function () { ); }); - test('JSO', function () { + test('JSO', async function () { const block = createRenderedBlock(this.workspace, 'jso_block'); const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE); - icon.setBubbleVisible(true); + await icon.setBubbleVisible(true); const mutatorWorkspace = icon.getWorkspace(); mutatorWorkspace .getBlockById('check_block') diff --git a/tests/mocha/old_workspace_comment_test.js b/tests/mocha/old_workspace_comment_test.js new file mode 100644 index 000000000..3bef493f3 --- /dev/null +++ b/tests/mocha/old_workspace_comment_test.js @@ -0,0 +1,267 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite.skip('Workspace comment', function () { + setup(function () { + sharedTestSetup.call(this); + this.workspace = new Blockly.Workspace(); + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + suite('getTopComments(ordered=true)', function () { + test('No comments', function () { + chai.assert.equal(this.workspace.getTopComments(true).length, 0); + }); + + test('One comment', function () { + const comment = new Blockly.WorkspaceComment( + this.workspace, + 'comment text', + 0, + 0, + 'comment id', + ); + chai.assert.equal(this.workspace.getTopComments(true).length, 1); + chai.assert.equal(this.workspace.commentDB.get('comment id'), comment); + }); + + test('After clear empty workspace', function () { + this.workspace.clear(); + chai.assert.equal(this.workspace.getTopComments(true).length, 0); + }); + + test('After clear non-empty workspace', function () { + new Blockly.WorkspaceComment( + this.workspace, + 'comment text', + 0, + 0, + 'comment id', + ); + this.workspace.clear(); + chai.assert.equal(this.workspace.getTopComments(true).length, 0); + chai.assert.isFalse(this.workspace.commentDB.has('comment id')); + }); + + test('After dispose', function () { + const comment = new Blockly.WorkspaceComment( + this.workspace, + 'comment text', + 0, + 0, + 'comment id', + ); + comment.dispose(); + chai.assert.equal(this.workspace.getTopComments(true).length, 0); + chai.assert.isFalse(this.workspace.commentDB.has('comment id')); + }); + }); + + suite('getTopComments(ordered=false)', function () { + test('No comments', function () { + chai.assert.equal(this.workspace.getTopComments(false).length, 0); + }); + + test('One comment', function () { + const comment = new Blockly.WorkspaceComment( + this.workspace, + 'comment text', + 0, + 0, + 'comment id', + ); + chai.assert.equal(this.workspace.getTopComments(false).length, 1); + chai.assert.equal(this.workspace.commentDB.get('comment id'), comment); + }); + + test('After clear empty workspace', function () { + this.workspace.clear(); + chai.assert.equal(this.workspace.getTopComments(false).length, 0); + }); + + test('After clear non-empty workspace', function () { + new Blockly.WorkspaceComment( + this.workspace, + 'comment text', + 0, + 0, + 'comment id', + ); + this.workspace.clear(); + chai.assert.equal(this.workspace.getTopComments(false).length, 0); + chai.assert.isFalse(this.workspace.commentDB.has('comment id')); + }); + + test('After dispose', function () { + const comment = new Blockly.WorkspaceComment( + this.workspace, + 'comment text', + 0, + 0, + 'comment id', + ); + comment.dispose(); + chai.assert.equal(this.workspace.getTopComments(false).length, 0); + chai.assert.isFalse(this.workspace.commentDB.has('comment id')); + }); + }); + + suite('getCommentById', function () { + test('Trivial', function () { + const comment = new Blockly.WorkspaceComment( + this.workspace, + 'comment text', + 0, + 0, + 'comment id', + ); + chai.assert.equal(this.workspace.getCommentById(comment.id), comment); + }); + + test('Null id', function () { + chai.assert.isNull(this.workspace.getCommentById(null)); + }); + + test('Non-existent id', function () { + chai.assert.isNull(this.workspace.getCommentById('badId')); + }); + + test('After dispose', function () { + const comment = new Blockly.WorkspaceComment( + this.workspace, + 'comment text', + 0, + 0, + 'comment id', + ); + comment.dispose(); + chai.assert.isNull(this.workspace.getCommentById(comment.id)); + }); + }); + + suite('dispose', function () { + test('Called twice', function () { + const comment = new Blockly.WorkspaceComment( + this.workspace, + 'comment text', + 0, + 0, + 'comment id', + ); + comment.dispose(); + // Nothing should go wrong the second time dispose is called. + comment.dispose(); + }); + }); + + suite('Width and height', function () { + setup(function () { + this.comment = new Blockly.WorkspaceComment( + this.workspace, + 'comment text', + 10, + 20, + 'comment id', + ); + }); + + test('Initial values', function () { + chai.assert.equal(this.comment.getWidth(), 20, 'Width'); + chai.assert.equal(this.comment.getHeight(), 10, 'Height'); + }); + + test('setWidth does not affect height', function () { + this.comment.setWidth(30); + chai.assert.equal(this.comment.getWidth(), 30, 'Width'); + chai.assert.equal(this.comment.getHeight(), 10, 'Height'); + }); + + test('setHeight does not affect width', function () { + this.comment.setHeight(30); + chai.assert.equal(this.comment.getWidth(), 20, 'Width'); + chai.assert.equal(this.comment.getHeight(), 30, 'Height'); + }); + }); + + suite('XY position', function () { + setup(function () { + this.comment = new Blockly.WorkspaceComment( + this.workspace, + 'comment text', + 10, + 20, + 'comment id', + ); + }); + + test('Initial position', function () { + const xy = this.comment.getRelativeToSurfaceXY(); + chai.assert.equal(xy.x, 0, 'Initial X position'); + chai.assert.equal(xy.y, 0, 'Initial Y position'); + }); + + test('moveBy', function () { + this.comment.moveBy(10, 100); + const xy = this.comment.getRelativeToSurfaceXY(); + chai.assert.equal(xy.x, 10, 'New X position'); + chai.assert.equal(xy.y, 100, 'New Y position'); + }); + }); + + suite('Content', function () { + setup(function () { + this.comment = new Blockly.WorkspaceComment( + this.workspace, + 'comment text', + 0, + 0, + 'comment id', + ); + }); + + teardown(function () { + sinon.restore(); + }); + + test('After creation', function () { + chai.assert.equal(this.comment.getContent(), 'comment text'); + chai.assert.equal( + this.workspace.undoStack_.length, + 1, + 'Workspace undo stack', + ); + }); + + test('Set to same value', function () { + this.comment.setContent('comment text'); + chai.assert.equal(this.comment.getContent(), 'comment text'); + // Setting the text to the old value does not fire an event. + chai.assert.equal( + this.workspace.undoStack_.length, + 1, + 'Workspace undo stack', + ); + }); + + test('Set to different value', function () { + this.comment.setContent('new comment text'); + chai.assert.equal(this.comment.getContent(), 'new comment text'); + chai.assert.equal( + this.workspace.undoStack_.length, + 2, + 'Workspace undo stack', + ); + }); + }); +}); diff --git a/tests/mocha/render_management_test.js b/tests/mocha/render_management_test.js index 4a69d2bab..7852a5b0c 100644 --- a/tests/mocha/render_management_test.js +++ b/tests/mocha/render_management_test.js @@ -32,6 +32,8 @@ suite('Render Management', function () { isDisposed: () => false, getRelativeToSurfaceXY: () => ({x: 0, y: 0}), updateComponentLocations: () => {}, + bumpNeighbours: () => {}, + initialized: true, workspace: { resizeContents: () => {}, }, @@ -72,6 +74,8 @@ suite('Render Management', function () { isDisposed: () => false, getRelativeToSurfaceXY: () => ({x: 0, y: 0}), updateComponentLocations: () => {}, + bumpNeighbours: () => {}, + initialized: true, workspace: ws || createMockWorkspace(), }; } diff --git a/tests/mocha/serializer_test.js b/tests/mocha/serializer_test.js index bd9a47344..b10f48df5 100644 --- a/tests/mocha/serializer_test.js +++ b/tests/mocha/serializer_test.js @@ -81,7 +81,13 @@ Serializer.Attributes.Collapsed = new SerializerTestCase( Serializer.Attributes.Disabled = new SerializerTestCase( 'Disabled', '' + - '' + + '' + + '', +); +Serializer.Attributes.DisabledWithEncodedComma = new SerializerTestCase( + 'DisabledWithEncodedComma', + '' + + '' + '', ); Serializer.Attributes.NotDeletable = new SerializerTestCase( @@ -106,6 +112,7 @@ Serializer.Attributes.testCases = [ Serializer.Attributes.Basic, Serializer.Attributes.Collapsed, Serializer.Attributes.Disabled, + Serializer.Attributes.DisabledWithEncodedComma, Serializer.Attributes.NotDeletable, Serializer.Attributes.NotMovable, Serializer.Attributes.NotEditable, @@ -223,55 +230,6 @@ Serializer.Attributes.testSuites = [ Serializer.Fields = new SerializerTestSuite('Fields'); -Serializer.Fields.Angle = new SerializerTestSuite('Angle'); -Serializer.Fields.Angle.Simple = new SerializerTestCase( - 'Simple', - '' + - '' + - '90' + - '' + - '', -); -Serializer.Fields.Angle.Negative = new SerializerTestCase( - 'Negative', - '' + - '' + - '-90' + - '' + - '', -); -Serializer.Fields.Angle.Decimals = new SerializerTestCase( - 'Decimals', - '' + - '' + - '1.5' + - '' + - '', -); -Serializer.Fields.Angle.MaxPrecision = new SerializerTestCase( - 'MaxPrecision', - '' + - '' + - '1.000000000000001' + - '' + - '', -); -Serializer.Fields.Angle.SmallestNumber = new SerializerTestCase( - 'SmallestNumber', - '' + - '' + - '5e-324' + - '' + - '', -); -Serializer.Fields.Angle.testCases = [ - Serializer.Fields.Angle.Simple, - Serializer.Fields.Angle.Negative, - Serializer.Fields.Angle.Decimals, - Serializer.Fields.Angle.MaxPrecision, - Serializer.Fields.Angle.SmallestNumber, -]; - Serializer.Fields.Checkbox = new SerializerTestSuite('Checkbox'); Serializer.Fields.Checkbox.True = new SerializerTestCase( 'True', @@ -294,37 +252,6 @@ Serializer.Fields.Checkbox.testCases = [ Serializer.Fields.Checkbox.False, ]; -Serializer.Fields.Colour = new SerializerTestSuite('Colour'); -Serializer.Fields.Colour.ThreeChar = new SerializerTestCase( - 'ThreeChar', - '' + - '' + - '#ffcc00' + // Could use a 3 char code. - '' + - '', -); -Serializer.Fields.Colour.SixChar = new SerializerTestCase( - 'SixChar', - '' + - '' + - '#f1c101' + - '' + - '', -); -Serializer.Fields.Colour.Black = new SerializerTestCase( - 'Black', - '' + - '' + - '#000000' + - '' + - '', -); -Serializer.Fields.Colour.testCases = [ - Serializer.Fields.Colour.ThreeChar, - Serializer.Fields.Colour.SixChar, - Serializer.Fields.Colour.Black, -]; - Serializer.Fields.Dropdown = new SerializerTestSuite('Dropdown'); Serializer.Fields.Dropdown.Default = new SerializerTestCase( 'Default', @@ -462,141 +389,6 @@ Serializer.Fields.LabelSerializable.testCases = [ // Serializer.Fields.LabelSerializable.ControlChars, ]; -Serializer.Fields.MultilineInput = new SerializerTestSuite('MultilineInput'); -Serializer.Fields.MultilineInput.SingleLine = new SerializerTestCase( - 'SingleLine', - '' + - '' + - 'test' + - '' + - '', -); -Serializer.Fields.MultilineInput.MultipleLines = new SerializerTestCase( - 'MultipleLines', - '' + - '' + - 'line1&#10;line2&#10;line3' + - '' + - '', -); -Serializer.Fields.MultilineInput.Indentation = new SerializerTestCase( - 'Indentation', - '' + - '' + - 'line1&#10; line2&#10; line3' + - '' + - '', -); -/* eslint-disable no-tabs */ -Serializer.Fields.MultilineInput.Tabs = new SerializerTestCase( - 'Tabs', - '' + - '' + - '' + - 'line1&#10;&#x9line2&#10;&#x9line3' + - '' + - '' + - '', -); -/* eslint-enable no-tabs */ -Serializer.Fields.MultilineInput.Symbols = new SerializerTestCase( - 'Symbols', - '' + - '' + - '~`!@#$%^*()_+-={[}]|\\:;,.?/' + - '' + - '', -); -Serializer.Fields.MultilineInput.EscapedSymbols = new SerializerTestCase( - 'EscapedSymbols', - '' + - '' + - '&<>' + - '' + - '', -); -Serializer.Fields.MultilineInput.SingleQuotes = new SerializerTestCase( - 'SingleQuotes', - '' + - '' + - '\'test\'' + - '' + - '', -); -Serializer.Fields.MultilineInput.DoubleQuotes = new SerializerTestCase( - 'DoubleQuotes', - '' + - '' + - '"test"' + - '' + - '', -); -Serializer.Fields.MultilineInput.Numbers = new SerializerTestCase( - 'Numbers', - '' + - '' + - '1234567890a123a123a' + - '' + - '', -); -Serializer.Fields.MultilineInput.Emoji = new SerializerTestCase( - 'Emoji', - '' + - '' + - '😀👋🏿👋🏾👋🏽👋🏼👋🏻😀❤❤❤' + - '' + - '', -); -Serializer.Fields.MultilineInput.Russian = new SerializerTestCase( - 'Russian', - '' + - '' + - 'ты любопытный кот' + - '' + - '', -); -Serializer.Fields.MultilineInput.Japanese = new SerializerTestCase( - 'Japanese', - '' + - '' + - 'あなたは好奇心旺盛な猫です' + - '' + - '', -); -Serializer.Fields.MultilineInput.Zalgo = new SerializerTestCase( - 'Zalgo', - '' + - '' + - 'z̴̪͈̲̜͕̽̈̀͒͂̓̋̉̍a̸̧̧̜̻̘̤̫̱̲͎̞̻͆̋ļ̸̛̖̜̳͚̖͔̟̈́͂̉̀͑̑͑̎ǵ̸̫̳̽̐̃̑̚̕o̶͇̫͔̮̼̭͕̹̘̬͋̀͆̂̇̋͊̒̽' + - '' + - '', -); -Serializer.Fields.MultilineInput.ControlChars = new SerializerTestCase( - 'ControlChars', - '' + - '' + - '&#a1;' + - '' + - '', -); -Serializer.Fields.MultilineInput.testCases = [ - Serializer.Fields.MultilineInput.SingleLine, - Serializer.Fields.MultilineInput.MultipleLines, - Serializer.Fields.MultilineInput.Indentation, - Serializer.Fields.MultilineInput.Tabs, - Serializer.Fields.MultilineInput.Symbols, - Serializer.Fields.MultilineInput.EscapedSymbols, - Serializer.Fields.MultilineInput.SingleQuotes, - Serializer.Fields.MultilineInput.DoubleQuotes, - Serializer.Fields.MultilineInput.Numbers, - Serializer.Fields.MultilineInput.Emoji, - Serializer.Fields.MultilineInput.Russian, - Serializer.Fields.MultilineInput.Japanese, - Serializer.Fields.MultilineInput.Zalgo, - // TODO: Uncoment once #4945 is merged. - // Serializer.Fields.MultilineInput.ControlChars, -]; - Serializer.Fields.Number = new SerializerTestSuite('Number'); Serializer.Fields.Number.Simple = new SerializerTestCase( 'Simple', @@ -1070,12 +862,9 @@ Serializer.Fields.Variable.Id.testSuites = [ Serializer.Fields.Variable.testSuites = [Serializer.Fields.Variable.Id]; Serializer.Fields.testSuites = [ - Serializer.Fields.Angle, Serializer.Fields.Checkbox, - Serializer.Fields.Colour, Serializer.Fields.Dropdown, Serializer.Fields.LabelSerializable, - Serializer.Fields.MultilineInput, Serializer.Fields.Number, Serializer.Fields.TextInput, Serializer.Fields.Variable, @@ -2086,12 +1875,181 @@ Serializer.Mutations.testSuites = [ Serializer.Mutations.Procedure, ]; +Serializer.Comments = new SerializerTestSuite('Comments'); + +Serializer.Comments.Coordinates = new SerializerTestSuite('Coordinates'); +Serializer.Comments.Coordinates.Basic = new SerializerTestCase( + 'Basic', + '' + + '' + + '' + + '', +); +Serializer.Comments.Coordinates.Negative = new SerializerTestCase( + 'Negative', + '' + + '' + + '' + + '', +); +Serializer.Comments.Coordinates.Zero = new SerializerTestCase( + 'Zero', + '' + + '' + + '' + + '', +); +Serializer.Comments.Coordinates.testCases = [ + Serializer.Comments.Coordinates.Basic, + Serializer.Comments.Coordinates.Negative, + Serializer.Comments.Coordinates.Zero, +]; + +Serializer.Comments.Size = new SerializerTestSuite('Size'); +Serializer.Comments.Size.Basic = new SerializerTestCase( + 'Basic', + '' + + '' + + '' + + '', +); +Serializer.Comments.Size.testCases = [Serializer.Comments.Size.Basic]; + +Serializer.Comments.Text = new SerializerTestSuite('Text'); +Serializer.Comments.Text.Symbols = new SerializerTestCase( + 'Symbols', + '' + + '' + + '~`!@#$%^*()_+-={[}]|\\:;,.?/' + + '' + + '', +); +Serializer.Comments.Text.EscapedSymbols = new SerializerTestCase( + 'EscapedSymbols', + '' + + '' + + '&<>' + + '' + + '', +); +Serializer.Comments.Text.SingleQuotes = new SerializerTestCase( + 'SingleQuotes', + '' + + '' + + "'test'" + + '' + + '', +); +Serializer.Comments.Text.DoubleQuotes = new SerializerTestCase( + 'DoubleQuotes', + '' + + '' + + '"test"' + + '' + + '', +); +Serializer.Comments.Text.Numbers = new SerializerTestCase( + 'Numbers', + '' + + '' + + '1234567890a123a123a' + + '' + + '', +); +Serializer.Comments.Text.Emoji = new SerializerTestCase( + 'Emoji', + '' + + '' + + '😀👋🏿👋🏾👋🏽👋🏼👋🏻😀❤❤❤' + + '' + + '', +); +Serializer.Comments.Text.Russian = new SerializerTestCase( + 'Russian', + '' + + '' + + 'ты любопытный кот' + + '' + + '', +); +Serializer.Comments.Text.Japanese = new SerializerTestCase( + 'Japanese', + '' + + '' + + 'あなたは好奇心旺盛な猫です' + + '' + + '', +); +Serializer.Comments.Text.Zalgo = new SerializerTestCase( + 'Zalgo', + '' + + '' + + 'z̴̪͈̲̜͕̽̈̀͒͂̓̋̉̍a̸̧̧̜̻̘̤̫̱̲͎̞̻͆̋ļ̸̛̖̜̳͚̖͔̟̈́͂̉̀͑̑͑̎ǵ̸̫̳̽̐̃̑̚̕o̶͇̫͔̮̼̭͕̹̘̬͋̀͆̂̇̋͊̒̽' + + '' + + '', +); +Serializer.Comments.Text.testCases = [ + Serializer.Comments.Text.Symbols, + Serializer.Comments.Text.EscapedSymbols, + Serializer.Comments.Text.SingleQuotes, + Serializer.Comments.Text.DoubleQuotes, + Serializer.Comments.Text.Numbers, + Serializer.Comments.Text.Emoji, + Serializer.Comments.Text.Russian, + Serializer.Comments.Text.Japanese, + Serializer.Comments.Text.Zalgo, +]; + +Serializer.Comments.Attributes = new SerializerTestSuite('Attributes'); +Serializer.Comments.Attributes.Collapsed = new SerializerTestCase( + 'Collapsed', + '' + + '' + + '' + + '', +); +Serializer.Comments.Attributes.NotEditable = new SerializerTestCase( + 'NotEditable', + '' + + '' + + '' + + '', +); +Serializer.Comments.Attributes.NotMovable = new SerializerTestCase( + 'NotMovable', + '' + + '' + + '' + + '', +); +Serializer.Comments.Attributes.NotDeletable = new SerializerTestCase( + 'NotDeletable', + '' + + '' + + '' + + '', +); +Serializer.Comments.Attributes.testCases = [ + Serializer.Comments.Attributes.Collapsed, + Serializer.Comments.Attributes.NotEditable, + Serializer.Comments.Attributes.NotMovable, + Serializer.Comments.Attributes.NotDeletable, +]; + +Serializer.Comments.testSuites = [ + Serializer.Comments.Coordinates, + Serializer.Comments.Size, + Serializer.Comments.Text, + Serializer.Comments.Attributes, +]; + Serializer.testSuites = [ Serializer.Attributes, Serializer.Fields, Serializer.Icons, Serializer.Connections, Serializer.Mutations, + Serializer.Comments, ]; const runSerializerTestSuite = (serializer, deserializer, testSuite) => { diff --git a/tests/mocha/test_helpers/procedures.js b/tests/mocha/test_helpers/procedures.js index e4ddc0e3f..4717b38c8 100644 --- a/tests/mocha/test_helpers/procedures.js +++ b/tests/mocha/test_helpers/procedures.js @@ -189,6 +189,14 @@ export class MockProcedureModel { this.enabled = true; } + static loadState(state, workspace) { + return new MockProcedureModel(); + } + + saveState() { + return {}; + } + setName(name) { this.name = name; return this; @@ -250,6 +258,14 @@ export class MockParameterModel { this.types = []; } + static loadState(state, workspace) { + return new MockParameterModel('test'); + } + + saveState() { + return {}; + } + setName(name) { this.name = name; return this; diff --git a/tests/mocha/test_helpers/workspace.js b/tests/mocha/test_helpers/workspace.js index 7654feba2..6da113f8b 100644 --- a/tests/mocha/test_helpers/workspace.js +++ b/tests/mocha/test_helpers/workspace.js @@ -781,7 +781,9 @@ export function testAWorkspace() { const xml = Blockly.utils.xml.textToDom(xmlText); Blockly.Xml.domToBlock(xml, this.workspace); this.workspace.getTopBlocks()[0].dispose(false); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); const newXml = Blockly.Xml.workspaceToDom(this.workspace); assertNodesEqual(newXml.firstChild, xml); } @@ -909,11 +911,14 @@ export function testAWorkspace() { function testUndoConnect(xmlText, parentId, childId, func) { const xml = Blockly.utils.xml.textToDom(xmlText); Blockly.Xml.domToWorkspace(xml, this.workspace); + this.clock.runAll(); const parent = this.workspace.getBlockById(parentId); const child = this.workspace.getBlockById(childId); func.call(this, parent, child); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); const newXml = Blockly.Xml.workspaceToDom(this.workspace); assertNodesEqual(newXml, xml); @@ -922,8 +927,8 @@ export function testAWorkspace() { test('Stack', function () { const xml = '' + - ' ' + - ' ' + + ' ' + + ' ' + ''; testUndoConnect.call(this, xml, '1', '2', (parent, child) => { @@ -934,8 +939,8 @@ export function testAWorkspace() { test('Row', function () { const xml = '' + - ' ' + - ' ' + + ' ' + + ' ' + ''; testUndoConnect.call(this, xml, '1', '2', (parent, child) => { @@ -946,8 +951,8 @@ export function testAWorkspace() { test('Statement', function () { const xml = '' + - ' ' + - ' ' + + ' ' + + ' ' + ''; testUndoConnect.call(this, xml, '1', '2', (parent, child) => { @@ -960,12 +965,12 @@ export function testAWorkspace() { test('Stack w/ child', function () { const xml = '' + - ' ' + + ' ' + ' ' + ' ' + ' ' + ' ' + - ' ' + + ' ' + ''; testUndoConnect.call(this, xml, '1', '2', (parent, child) => { @@ -976,12 +981,12 @@ export function testAWorkspace() { test('Row w/ child', function () { const xml = '' + - ' ' + + ' ' + ' ' + ' ' + ' ' + ' ' + - ' ' + + ' ' + ''; testUndoConnect.call(this, xml, '1', '2', (parent, child) => { @@ -992,12 +997,12 @@ export function testAWorkspace() { test('Statement w/ child', function () { const xml = '' + - ' ' + + ' ' + ' ' + ' ' + ' ' + ' ' + - ' ' + + ' ' + ''; testUndoConnect.call(this, xml, '1', '2', (parent, child) => { @@ -1010,12 +1015,12 @@ export function testAWorkspace() { test('Stack w/ shadow', function () { const xml = '' + - ' ' + + ' ' + ' ' + ' ' + ' ' + ' ' + - ' ' + + ' ' + ''; testUndoConnect.call(this, xml, '1', '2', (parent, child) => { @@ -1026,12 +1031,12 @@ export function testAWorkspace() { test('Row w/ shadow', function () { const xml = '' + - ' ' + + ' ' + ' ' + ' ' + ' ' + ' ' + - ' ' + + ' ' + ''; testUndoConnect.call(this, xml, '1', '2', (parent, child) => { @@ -1042,12 +1047,12 @@ export function testAWorkspace() { test('Statement w/ shadow', function () { const xml = '' + - ' ' + + ' ' + ' ' + ' ' + ' ' + ' ' + - ' ' + + ' ' + ''; testUndoConnect.call(this, xml, '1', '2', (parent, child) => { @@ -1102,6 +1107,7 @@ export function testAWorkspace() { function testUndoDisconnect(xmlText, childId) { const xml = Blockly.utils.xml.textToDom(xmlText); Blockly.Xml.domToWorkspace(xml, this.workspace); + this.clock.runAll(); const child = this.workspace.getBlockById(childId); if (child.outputConnection) { @@ -1109,7 +1115,9 @@ export function testAWorkspace() { } else { child.previousConnection.disconnect(); } + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); const newXml = Blockly.Xml.workspaceToDom(this.workspace); assertNodesEqual(newXml, xml); @@ -1262,8 +1270,10 @@ export function testAWorkspace() { suite('createVariable', function () { test('Undo only', function () { createTwoVarsDifferentTypes(this.workspace); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); chai.assert.isNull(this.workspace.getVariableById('id2')); @@ -1274,8 +1284,10 @@ export function testAWorkspace() { test('Undo and redo', function () { createTwoVarsDifferentTypes(this.workspace); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); chai.assert.isNull(this.workspace.getVariableById('id2')); @@ -1300,14 +1312,18 @@ export function testAWorkspace() { suite('deleteVariableById', function () { test('Undo only no usages', function () { createTwoVarsDifferentTypes(this.workspace); + this.clock.runAll(); this.workspace.deleteVariableById('id1'); this.workspace.deleteVariableById('id2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); chai.assert.isNull(this.workspace.getVariableById('id1')); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); }); @@ -1316,15 +1332,19 @@ export function testAWorkspace() { createTwoVarsDifferentTypes(this.workspace); // Create blocks to refer to both of them. createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); + this.clock.runAll(); this.workspace.deleteVariableById('id1'); this.workspace.deleteVariableById('id2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name2'); chai.assert.isNull(this.workspace.getVariableById('id1')); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(); + this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name2'); assertBlockVarModelName(this.workspace, 1, 'name1'); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); @@ -1333,24 +1353,31 @@ export function testAWorkspace() { test('Reference exists no usages', function () { createTwoVarsDifferentTypes(this.workspace); + this.clock.runAll(); this.workspace.deleteVariableById('id1'); this.workspace.deleteVariableById('id2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); chai.assert.isNull(this.workspace.getVariableById('id1')); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(true); + this.clock.runAll(); // Expect that both variables are deleted chai.assert.isNull(this.workspace.getVariableById('id1')); chai.assert.isNull(this.workspace.getVariableById('id2')); this.workspace.undo(); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(true); + this.clock.runAll(); // Expect that variable 'id2' is recreated chai.assert.isNull(this.workspace.getVariableById('id1')); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); @@ -1360,28 +1387,35 @@ export function testAWorkspace() { createTwoVarsDifferentTypes(this.workspace); // Create blocks to refer to both of them. createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); + this.clock.runAll(); this.workspace.deleteVariableById('id1'); this.workspace.deleteVariableById('id2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name2'); chai.assert.isNull(this.workspace.getVariableById('id1')); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(true); + this.clock.runAll(); // Expect that both variables are deleted chai.assert.equal(this.workspace.getTopBlocks(false).length, 0); chai.assert.isNull(this.workspace.getVariableById('id1')); chai.assert.isNull(this.workspace.getVariableById('id2')); this.workspace.undo(); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name2'); assertBlockVarModelName(this.workspace, 1, 'name1'); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(true); + this.clock.runAll(); // Expect that variable 'id2' is recreated assertBlockVarModelName(this.workspace, 0, 'name2'); chai.assert.isNull(this.workspace.getVariableById('id1')); @@ -1391,6 +1425,7 @@ export function testAWorkspace() { test('Delete same variable twice no usages', function () { this.workspace.createVariable('name1', 'type1', 'id1'); this.workspace.deleteVariableById('id1'); + this.clock.runAll(); const workspace = this.workspace; assertWarnings(() => { workspace.deleteVariableById('id1'); @@ -1406,21 +1441,26 @@ export function testAWorkspace() { // Undo delete this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); // Redo delete this.workspace.undo(true); + this.clock.runAll(); chai.assert.isNull(this.workspace.getVariableById('id1')); // Redo delete, nothing should happen this.workspace.undo(true); + this.clock.runAll(); chai.assert.isNull(this.workspace.getVariableById('id1')); }); test('Delete same variable twice with usages', function () { this.workspace.createVariable('name1', 'type1', 'id1'); createVarBlocksNoEvents(this.workspace, ['id1']); + this.clock.runAll(); this.workspace.deleteVariableById('id1'); + this.clock.runAll(); const workspace = this.workspace; assertWarnings(() => { workspace.deleteVariableById('id1'); @@ -1437,16 +1477,19 @@ export function testAWorkspace() { // Undo delete this.workspace.undo(); + this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name1'); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); // Redo delete this.workspace.undo(true); + this.clock.runAll(); chai.assert.equal(this.workspace.getTopBlocks(false).length, 0); chai.assert.isNull(this.workspace.getVariableById('id1')); // Redo delete, nothing should happen this.workspace.undo(true); + this.clock.runAll(); chai.assert.equal(this.workspace.getTopBlocks(false).length, 0); chai.assert.isNull(this.workspace.getVariableById('id1')); }); @@ -1459,46 +1502,58 @@ export function testAWorkspace() { test('Reference exists no usages rename to name2', function () { this.workspace.renameVariableById('id1', 'name2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); this.workspace.undo(true); + this.clock.runAll(); assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); }); test('Reference exists with usages rename to name2', function () { createVarBlocksNoEvents(this.workspace, ['id1']); this.workspace.renameVariableById('id1', 'name2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name1'); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); this.workspace.undo(true); + this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name2'); assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); }); test('Reference exists different capitalization no usages rename to Name1', function () { this.workspace.renameVariableById('id1', 'Name1'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); this.workspace.undo(true); + this.clock.runAll(); assertVariableValues(this.workspace, 'Name1', 'type1', 'id1'); }); test('Reference exists different capitalization with usages rename to Name1', function () { createVarBlocksNoEvents(this.workspace, ['id1']); this.workspace.renameVariableById('id1', 'Name1'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name1'); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); this.workspace.undo(true); + this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'Name1'); assertVariableValues(this.workspace, 'Name1', 'type1', 'id1'); }); @@ -1507,12 +1562,15 @@ export function testAWorkspace() { test('Same type no usages rename variable with id1 to name2', function () { this.workspace.createVariable('name2', 'type1', 'id2'); this.workspace.renameVariableById('id1', 'name2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); this.workspace.undo(true); + this.clock.runAll(); assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); chai.assert.isNull(this.workspace.getVariableById('id1')); }); @@ -1521,14 +1579,17 @@ export function testAWorkspace() { this.workspace.createVariable('name2', 'type1', 'id2'); createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); this.workspace.renameVariableById('id1', 'name2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name1'); assertBlockVarModelName(this.workspace, 1, 'name2'); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); this.workspace.undo(true); + this.clock.runAll(); assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); chai.assert.isNull(this.workspace.getVariableById('id1')); }); @@ -1536,12 +1597,15 @@ export function testAWorkspace() { test('Same type different capitalization no usages rename variable with id1 to Name2', function () { this.workspace.createVariable('name2', 'type1', 'id2'); this.workspace.renameVariableById('id1', 'Name2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); this.workspace.undo(true); + this.clock.runAll(); assertVariableValues(this.workspace, 'Name2', 'type1', 'id2'); chai.assert.isNull(this.workspace.getVariable('name1')); }); @@ -1550,14 +1614,17 @@ export function testAWorkspace() { this.workspace.createVariable('name2', 'type1', 'id2'); createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); this.workspace.renameVariableById('id1', 'Name2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name1'); assertBlockVarModelName(this.workspace, 1, 'name2'); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); this.workspace.undo(true); + this.clock.runAll(); assertVariableValues(this.workspace, 'Name2', 'type1', 'id2'); chai.assert.isNull(this.workspace.getVariableById('id1')); assertBlockVarModelName(this.workspace, 0, 'Name2'); @@ -1567,12 +1634,15 @@ export function testAWorkspace() { test('Different type no usages rename variable with id1 to name2', function () { this.workspace.createVariable('name2', 'type2', 'id2'); this.workspace.renameVariableById('id1', 'name2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(true); + this.clock.runAll(); assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); }); @@ -1581,14 +1651,17 @@ export function testAWorkspace() { this.workspace.createVariable('name2', 'type2', 'id2'); createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); this.workspace.renameVariableById('id1', 'name2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); assertBlockVarModelName(this.workspace, 0, 'name1'); assertBlockVarModelName(this.workspace, 1, 'name2'); this.workspace.undo(true); + this.clock.runAll(); assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); assertBlockVarModelName(this.workspace, 0, 'name2'); @@ -1598,12 +1671,15 @@ export function testAWorkspace() { test('Different type different capitalization no usages rename variable with id1 to Name2', function () { this.workspace.createVariable('name2', 'type2', 'id2'); this.workspace.renameVariableById('id1', 'Name2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(true); + this.clock.runAll(); assertVariableValues(this.workspace, 'Name2', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); }); @@ -1612,14 +1688,17 @@ export function testAWorkspace() { this.workspace.createVariable('name2', 'type2', 'id2'); createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); this.workspace.renameVariableById('id1', 'Name2'); + this.clock.runAll(); this.workspace.undo(); + this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); assertBlockVarModelName(this.workspace, 0, 'name1'); assertBlockVarModelName(this.workspace, 1, 'name2'); this.workspace.undo(true); + this.clock.runAll(); assertVariableValues(this.workspace, 'Name2', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); assertBlockVarModelName(this.workspace, 0, 'Name2'); diff --git a/tests/mocha/workspace_comment_test.js b/tests/mocha/workspace_comment_test.js index f2126dea2..977d82aa2 100644 --- a/tests/mocha/workspace_comment_test.js +++ b/tests/mocha/workspace_comment_test.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2020 Google LLC + * Copyright 2024 Google LLC * SPDX-License-Identifier: Apache-2.0 */ @@ -8,259 +8,129 @@ import { sharedTestSetup, sharedTestTeardown, } from './test_helpers/setup_teardown.js'; +import { + createChangeListenerSpy, + assertEventFired, +} from './test_helpers/events.js'; suite('Workspace comment', function () { setup(function () { sharedTestSetup.call(this); - this.workspace = new Blockly.Workspace(); + this.workspace = new Blockly.inject('blocklyDiv', {}); }); teardown(function () { sharedTestTeardown.call(this); }); - suite('getTopComments(ordered=true)', function () { - test('No comments', function () { - chai.assert.equal(this.workspace.getTopComments(true).length, 0); - }); + suite('Events', function () { + test('create events are fired when a comment is constructed', function () { + const spy = createChangeListenerSpy(this.workspace); - test('One comment', function () { - const comment = new Blockly.WorkspaceComment( + this.renderedComment = new Blockly.comments.RenderedWorkspaceComment( this.workspace, - 'comment text', - 0, - 0, - 'comment id', ); - chai.assert.equal(this.workspace.getTopComments(true).length, 1); - chai.assert.equal(this.workspace.commentDB.get('comment id'), comment); + + assertEventFired( + spy, + Blockly.Events.CommentCreate, + {commentId: this.renderedComment.id}, + this.workspace.id, + ); }); - test('After clear empty workspace', function () { - this.workspace.clear(); - chai.assert.equal(this.workspace.getTopComments(true).length, 0); - }); - - test('After clear non-empty workspace', function () { - new Blockly.WorkspaceComment( + test('delete events are fired when a comment is disposed', function () { + this.renderedComment = new Blockly.comments.RenderedWorkspaceComment( this.workspace, - 'comment text', - 0, - 0, - 'comment id', ); - this.workspace.clear(); - chai.assert.equal(this.workspace.getTopComments(true).length, 0); - chai.assert.isFalse(this.workspace.commentDB.has('comment id')); + const spy = createChangeListenerSpy(this.workspace); + + this.renderedComment.dispose(); + + assertEventFired( + spy, + Blockly.Events.CommentDelete, + {commentId: this.renderedComment.id}, + this.workspace.id, + ); }); - test('After dispose', function () { - const comment = new Blockly.WorkspaceComment( + test('move events are fired when a comment is moved', function () { + this.renderedComment = new Blockly.comments.RenderedWorkspaceComment( this.workspace, - 'comment text', - 0, - 0, - 'comment id', ); - comment.dispose(); - chai.assert.equal(this.workspace.getTopComments(true).length, 0); - chai.assert.isFalse(this.workspace.commentDB.has('comment id')); - }); - }); + const spy = createChangeListenerSpy(this.workspace); - suite('getTopComments(ordered=false)', function () { - test('No comments', function () { - chai.assert.equal(this.workspace.getTopComments(false).length, 0); + this.renderedComment.moveTo(new Blockly.utils.Coordinate(42, 42)); + + assertEventFired( + spy, + Blockly.Events.CommentMove, + { + commentId: this.renderedComment.id, + oldCoordinate_: {x: 0, y: 0}, + newCoordinate_: {x: 42, y: 42}, + }, + this.workspace.id, + ); }); - test('One comment', function () { - const comment = new Blockly.WorkspaceComment( + test('change events are fired when a comments text is edited', function () { + this.renderedComment = new Blockly.comments.RenderedWorkspaceComment( this.workspace, - 'comment text', - 0, - 0, - 'comment id', ); - chai.assert.equal(this.workspace.getTopComments(false).length, 1); - chai.assert.equal(this.workspace.commentDB.get('comment id'), comment); + const spy = createChangeListenerSpy(this.workspace); + + this.renderedComment.setText('test text'); + + assertEventFired( + spy, + Blockly.Events.CommentChange, + { + commentId: this.renderedComment.id, + oldContents_: '', + newContents_: 'test text', + }, + this.workspace.id, + ); }); - test('After clear empty workspace', function () { - this.workspace.clear(); - chai.assert.equal(this.workspace.getTopComments(false).length, 0); - }); - - test('After clear non-empty workspace', function () { - new Blockly.WorkspaceComment( + test('collapse events are fired when a comment is collapsed', function () { + this.renderedComment = new Blockly.comments.RenderedWorkspaceComment( this.workspace, - 'comment text', - 0, - 0, - 'comment id', ); - this.workspace.clear(); - chai.assert.equal(this.workspace.getTopComments(false).length, 0); - chai.assert.isFalse(this.workspace.commentDB.has('comment id')); + const spy = createChangeListenerSpy(this.workspace); + + this.renderedComment.setCollapsed(true); + + assertEventFired( + spy, + Blockly.Events.CommentCollapse, + { + commentId: this.renderedComment.id, + newCollapsed: true, + }, + this.workspace.id, + ); }); - test('After dispose', function () { - const comment = new Blockly.WorkspaceComment( + test('collapse events are fired when a comment is uncollapsed', function () { + this.renderedComment = new Blockly.comments.RenderedWorkspaceComment( this.workspace, - 'comment text', - 0, - 0, - 'comment id', ); - comment.dispose(); - chai.assert.equal(this.workspace.getTopComments(false).length, 0); - chai.assert.isFalse(this.workspace.commentDB.has('comment id')); - }); - }); + this.renderedComment.setCollapsed(true); + const spy = createChangeListenerSpy(this.workspace); - suite('getCommentById', function () { - test('Trivial', function () { - const comment = new Blockly.WorkspaceComment( - this.workspace, - 'comment text', - 0, - 0, - 'comment id', - ); - chai.assert.equal(this.workspace.getCommentById(comment.id), comment); - }); + this.renderedComment.setCollapsed(false); - test('Null id', function () { - chai.assert.isNull(this.workspace.getCommentById(null)); - }); - - test('Non-existent id', function () { - chai.assert.isNull(this.workspace.getCommentById('badId')); - }); - - test('After dispose', function () { - const comment = new Blockly.WorkspaceComment( - this.workspace, - 'comment text', - 0, - 0, - 'comment id', - ); - comment.dispose(); - chai.assert.isNull(this.workspace.getCommentById(comment.id)); - }); - }); - - suite('dispose', function () { - test('Called twice', function () { - const comment = new Blockly.WorkspaceComment( - this.workspace, - 'comment text', - 0, - 0, - 'comment id', - ); - comment.dispose(); - // Nothing should go wrong the second time dispose is called. - comment.dispose(); - }); - }); - - suite('Width and height', function () { - setup(function () { - this.comment = new Blockly.WorkspaceComment( - this.workspace, - 'comment text', - 10, - 20, - 'comment id', - ); - }); - - test('Initial values', function () { - chai.assert.equal(this.comment.getWidth(), 20, 'Width'); - chai.assert.equal(this.comment.getHeight(), 10, 'Height'); - }); - - test('setWidth does not affect height', function () { - this.comment.setWidth(30); - chai.assert.equal(this.comment.getWidth(), 30, 'Width'); - chai.assert.equal(this.comment.getHeight(), 10, 'Height'); - }); - - test('setHeight does not affect width', function () { - this.comment.setHeight(30); - chai.assert.equal(this.comment.getWidth(), 20, 'Width'); - chai.assert.equal(this.comment.getHeight(), 30, 'Height'); - }); - }); - - suite('XY position', function () { - setup(function () { - this.comment = new Blockly.WorkspaceComment( - this.workspace, - 'comment text', - 10, - 20, - 'comment id', - ); - }); - - test('Initial position', function () { - const xy = this.comment.getRelativeToSurfaceXY(); - chai.assert.equal(xy.x, 0, 'Initial X position'); - chai.assert.equal(xy.y, 0, 'Initial Y position'); - }); - - test('moveBy', function () { - this.comment.moveBy(10, 100); - const xy = this.comment.getRelativeToSurfaceXY(); - chai.assert.equal(xy.x, 10, 'New X position'); - chai.assert.equal(xy.y, 100, 'New Y position'); - }); - }); - - suite('Content', function () { - setup(function () { - this.comment = new Blockly.WorkspaceComment( - this.workspace, - 'comment text', - 0, - 0, - 'comment id', - ); - }); - - teardown(function () { - sinon.restore(); - }); - - test('After creation', function () { - chai.assert.equal(this.comment.getContent(), 'comment text'); - chai.assert.equal( - this.workspace.undoStack_.length, - 1, - 'Workspace undo stack', - ); - }); - - test('Set to same value', function () { - this.comment.setContent('comment text'); - chai.assert.equal(this.comment.getContent(), 'comment text'); - // Setting the text to the old value does not fire an event. - chai.assert.equal( - this.workspace.undoStack_.length, - 1, - 'Workspace undo stack', - ); - }); - - test('Set to different value', function () { - this.comment.setContent('new comment text'); - chai.assert.equal(this.comment.getContent(), 'new comment text'); - chai.assert.equal( - this.workspace.undoStack_.length, - 2, - 'Workspace undo stack', + assertEventFired( + spy, + Blockly.Events.CommentCollapse, + { + commentId: this.renderedComment.id, + newCollapsed: false, + }, + this.workspace.id, ); }); }); diff --git a/tests/mocha/workspace_svg_test.js b/tests/mocha/workspace_svg_test.js index cea58cdf9..98ca14d57 100644 --- a/tests/mocha/workspace_svg_test.js +++ b/tests/mocha/workspace_svg_test.js @@ -21,7 +21,7 @@ import {testAWorkspace} from './test_helpers/workspace.js'; suite('WorkspaceSvg', function () { setup(function () { - sharedTestSetup.call(this); + this.clock = sharedTestSetup.call(this, {fireEventsNow: false}).clock; const toolbox = document.getElementById('toolbox-categories'); this.workspace = Blockly.inject('blocklyDiv', {toolbox: toolbox}); Blockly.defineBlocksWithJsonArray([ @@ -168,8 +168,7 @@ suite('WorkspaceSvg', function () { }); suite('Viewport change events', function () { - function resetEventHistory(eventsFireStub, changeListenerSpy) { - eventsFireStub.resetHistory(); + function resetEventHistory(changeListenerSpy) { changeListenerSpy.resetHistory(); } function assertSpyFiredViewportEvent(spy, workspace, expectedProperties) { @@ -187,7 +186,6 @@ suite('WorkspaceSvg', function () { ); } function assertViewportEventFired( - eventsFireStub, changeListenerSpy, workspace, expectedEventCount = 1, @@ -200,32 +198,25 @@ suite('WorkspaceSvg', function () { viewLeft: metrics.viewLeft, type: eventUtils.VIEWPORT_CHANGE, }; - assertSpyFiredViewportEvent( - eventsFireStub, - workspace, - expectedProperties, - ); assertSpyFiredViewportEvent( changeListenerSpy, workspace, expectedProperties, ); sinon.assert.callCount(changeListenerSpy, expectedEventCount); - sinon.assert.callCount(eventsFireStub, expectedEventCount); } function runViewportEventTest( eventTriggerFunc, - eventsFireStub, changeListenerSpy, workspace, clock, expectedEventCount = 1, ) { clock.runAll(); - resetEventHistory(eventsFireStub, changeListenerSpy); + resetEventHistory(changeListenerSpy); eventTriggerFunc(); + clock.runAll(); assertViewportEventFired( - eventsFireStub, changeListenerSpy, workspace, expectedEventCount, @@ -243,7 +234,6 @@ suite('WorkspaceSvg', function () { test('setScale', function () { runViewportEventTest( () => this.workspace.setScale(2), - this.eventsFireStub, this.changeListenerSpy, this.workspace, this.clock, @@ -252,7 +242,6 @@ suite('WorkspaceSvg', function () { test('zoom(50, 50, 1)', function () { runViewportEventTest( () => this.workspace.zoom(50, 50, 1), - this.eventsFireStub, this.changeListenerSpy, this.workspace, this.clock, @@ -261,7 +250,6 @@ suite('WorkspaceSvg', function () { test('zoom(50, 50, -1)', function () { runViewportEventTest( () => this.workspace.zoom(50, 50, -1), - this.eventsFireStub, this.changeListenerSpy, this.workspace, this.clock, @@ -270,7 +258,6 @@ suite('WorkspaceSvg', function () { test('zoomCenter(1)', function () { runViewportEventTest( () => this.workspace.zoomCenter(1), - this.eventsFireStub, this.changeListenerSpy, this.workspace, this.clock, @@ -279,7 +266,6 @@ suite('WorkspaceSvg', function () { test('zoomCenter(-1)', function () { runViewportEventTest( () => this.workspace.zoomCenter(-1), - this.eventsFireStub, this.changeListenerSpy, this.workspace, this.clock, @@ -291,7 +277,6 @@ suite('WorkspaceSvg', function () { block.render(); runViewportEventTest( () => this.workspace.zoomToFit(), - this.eventsFireStub, this.changeListenerSpy, this.workspace, this.clock, @@ -305,7 +290,6 @@ suite('WorkspaceSvg', function () { block.render(); runViewportEventTest( () => this.workspace.centerOnBlock(block.id), - this.eventsFireStub, this.changeListenerSpy, this.workspace, this.clock, @@ -314,7 +298,6 @@ suite('WorkspaceSvg', function () { test('scroll', function () { runViewportEventTest( () => this.workspace.scroll(50, 50), - this.eventsFireStub, this.changeListenerSpy, this.workspace, this.clock, @@ -323,7 +306,6 @@ suite('WorkspaceSvg', function () { test('scrollCenter', function () { runViewportEventTest( () => this.workspace.scrollCenter(), - this.eventsFireStub, this.changeListenerSpy, this.workspace, this.clock, @@ -336,13 +318,12 @@ suite('WorkspaceSvg', function () { block.initSvg(); block.render(); this.clock.runAll(); - resetEventHistory(this.eventsFireStub, this.changeListenerSpy); + resetEventHistory(this.changeListenerSpy); // Expect 2 events, 1 move, 1 viewport runViewportEventTest( () => { block.moveBy(1000, 1000); }, - this.eventsFireStub, this.changeListenerSpy, this.workspace, this.clock, @@ -366,7 +347,7 @@ suite('WorkspaceSvg', function () { '', ); this.clock.runAll(); - resetEventHistory(this.eventsFireStub, this.changeListenerSpy); + resetEventHistory(this.changeListenerSpy); // Add block in center of other blocks, not triggering scroll. Blockly.Xml.domToWorkspace( Blockly.utils.xml.textToDom( @@ -375,11 +356,6 @@ suite('WorkspaceSvg', function () { this.workspace, ); this.clock.runAll(); - assertEventNotFired( - this.eventsFireStub, - Blockly.Events.ViewportChange, - {type: eventUtils.VIEWPORT_CHANGE}, - ); assertEventNotFired( this.changeListenerSpy, Blockly.Events.ViewportChange, @@ -403,15 +379,10 @@ suite('WorkspaceSvg', function () { '', ); this.clock.runAll(); - resetEventHistory(this.eventsFireStub, this.changeListenerSpy); + resetEventHistory(this.changeListenerSpy); // Add block in center of other blocks, not triggering scroll. Blockly.Xml.domToWorkspace(xmlDom, this.workspace); this.clock.runAll(); - assertEventNotFired( - this.eventsFireStub, - Blockly.Events.ViewportChange, - {type: eventUtils.VIEWPORT_CHANGE}, - ); assertEventNotFired( this.changeListenerSpy, Blockly.Events.ViewportChange, @@ -436,7 +407,6 @@ suite('WorkspaceSvg', function () { // Expect 10 events, 4 create, 4 move, 1 viewport, 1 finished loading runViewportEventTest( addingMultipleBlocks, - this.eventsFireStub, this.changeListenerSpy, this.workspace, this.clock, diff --git a/tests/mocha/xml_test.js b/tests/mocha/xml_test.js index 385b0f736..7bd16afd0 100644 --- a/tests/mocha/xml_test.js +++ b/tests/mocha/xml_test.js @@ -127,27 +127,6 @@ suite('XML', function () { workspaceTeardown.call(this, this.workspace); }); suite('Fields', function () { - test('Angle', function () { - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'field_angle_test_block', - 'message0': '%1', - 'args0': [ - { - 'type': 'field_angle', - 'name': 'ANGLE', - 'angle': 90, - }, - ], - }, - ]); - const block = new Blockly.Block( - this.workspace, - 'field_angle_test_block', - ); - const resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0]; - assertNonVariableField(resultFieldDom, 'ANGLE', '90'); - }); test('Checkbox', function () { Blockly.defineBlocksWithJsonArray([ { @@ -169,27 +148,6 @@ suite('XML', function () { const resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0]; assertNonVariableField(resultFieldDom, 'CHECKBOX', 'TRUE'); }); - test('Colour', function () { - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'field_colour_test_block', - 'message0': '%1', - 'args0': [ - { - 'type': 'field_colour', - 'name': 'COLOUR', - 'colour': '#000099', - }, - ], - }, - ]); - const block = new Blockly.Block( - this.workspace, - 'field_colour_test_block', - ); - const resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0]; - assertNonVariableField(resultFieldDom, 'COLOUR', '#000099'); - }); test('Dropdown', function () { Blockly.defineBlocksWithJsonArray([ { diff --git a/tests/multi_playground.html b/tests/multi_playground.html index f929de8d8..ff59127e6 100644 --- a/tests/multi_playground.html +++ b/tests/multi_playground.html @@ -342,7 +342,6 @@ - @@ -469,44 +468,6 @@ - - - - - - - 100 - - - - - 50 - - - - - 0 - - - - - - - #ff0000 - - - - - #3333ff - - - - - 0.5 - - - - - @@ -918,44 +916,6 @@ - - - - - - - 100 - - - - - 50 - - - - - 0 - - - - - - - #ff0000 - - - - - #3333ff - - - - - 0.5 - - - - - diff --git a/tests/typescript/src/generators.ts b/tests/typescript/src/generators.ts index a87d70ee3..fd79a3a00 100644 --- a/tests/typescript/src/generators.ts +++ b/tests/typescript/src/generators.ts @@ -28,4 +28,4 @@ testGenerator.forBlock['test_block'] = function ( return ['a fake code string', Order.ADDITION]; }; -phpGenerator.quote_(); +phpGenerator.quote_('a string'); diff --git a/tests/typescript/src/generators/dart.ts b/tests/typescript/src/generators/dart.ts new file mode 100644 index 000000000..ae285e5e8 --- /dev/null +++ b/tests/typescript/src/generators/dart.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from 'blockly-test/core'; + +/** + * Test: should be able to import a generator instance, class, and + * Order enum. + */ +import {dartGenerator, DartGenerator, Order} from 'blockly-test/dart'; + +/** + * Test: should be able to create a simple block generator function, + * correctly typed, and insert it into the .forBlock dictionary. + */ +dartGenerator.forBlock['the_answer'] = function ( + _block: Blockly.Block, + _generator: DartGenerator, +): [string, Order] { + return ['42', Order.ATOMIC]; +}; diff --git a/tests/typescript/src/generators/javascript.ts b/tests/typescript/src/generators/javascript.ts new file mode 100644 index 000000000..716c91459 --- /dev/null +++ b/tests/typescript/src/generators/javascript.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from 'blockly-test/core'; + +/** + * Test: should be able to import a generator instance, class, and + * Order enum. + */ +import { + javascriptGenerator, + JavascriptGenerator, + Order, +} from 'blockly-test/javascript'; + +/** + * Test: should be able to create a simple block generator function, + * correctly typed, and insert it into the .forBlock dictionary. + */ +javascriptGenerator.forBlock['the_answer'] = function ( + _block: Blockly.Block, + _generator: JavascriptGenerator, +): [string, Order] { + return ['42', Order.ATOMIC]; +}; diff --git a/tests/typescript/src/generators/lua.ts b/tests/typescript/src/generators/lua.ts new file mode 100644 index 000000000..b030ff7a7 --- /dev/null +++ b/tests/typescript/src/generators/lua.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from 'blockly-test/core'; + +/** + * Test: should be able to import a generator instance, class, and + * Order enum. + */ +import {luaGenerator, LuaGenerator, Order} from 'blockly-test/lua'; + +/** + * Test: should be able to create a simple block generator function, + * correctly typed, and insert it into the .forBlock dictionary. + */ +luaGenerator.forBlock['the_answer'] = function ( + _block: Blockly.Block, + _generator: LuaGenerator, +): [string, Order] { + return ['42', Order.ATOMIC]; +}; diff --git a/tests/typescript/src/generators/php.ts b/tests/typescript/src/generators/php.ts new file mode 100644 index 000000000..cb9241b43 --- /dev/null +++ b/tests/typescript/src/generators/php.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from 'blockly-test/core'; + +/** + * Test: should be able to import a generator instance, class, and + * Order enum. + */ +import {phpGenerator, PhpGenerator, Order} from 'blockly-test/php'; + +/** + * Test: should be able to create a simple block generator function, + * correctly typed, and insert it into the .forBlock dictionary. + */ +phpGenerator.forBlock['the_answer'] = function ( + _block: Blockly.Block, + _generator: PhpGenerator, +): [string, Order] { + return ['42', Order.ATOMIC]; +}; diff --git a/tests/typescript/src/generators/python.ts b/tests/typescript/src/generators/python.ts new file mode 100644 index 000000000..aa1c19c38 --- /dev/null +++ b/tests/typescript/src/generators/python.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from 'blockly-test/core'; + +/** + * Test: should be able to import a generator instance, class, and + * Order enum. + */ +import {pythonGenerator, PythonGenerator, Order} from 'blockly-test/python'; + +/** + * Test: should be able to create a simple block generator function, + * correctly typed, and insert it into the .forBlock dictionary. + */ +pythonGenerator.forBlock['the_answer'] = function ( + _block: Blockly.Block, + _generator: PythonGenerator, +): [string, Order] { + return ['42', Order.ATOMIC]; +}; diff --git a/tests/typescript/src/msg.ts b/tests/typescript/src/msg.ts new file mode 100644 index 000000000..beb63a612 --- /dev/null +++ b/tests/typescript/src/msg.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Test: Should be able to import messages and verify their type. + * Test at least one language other than English! + */ + +import * as en from 'blockly-test/msg/en'; +import * as fr from 'blockly-test/msg/fr'; + +let msg: {[key: string]: string}; +msg = fr; +msg = en; + +// Satisfy eslint that msg is used. +console.log(msg['DIALOG_OK']); diff --git a/typings/dart.d.ts b/typings/dart.d.ts index bf89e4f06..0f6b1e090 100644 --- a/typings/dart.d.ts +++ b/typings/dart.d.ts @@ -4,27 +4,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export enum Order { - ATOMIC = 0, // 0 "" ... - UNARY_POSTFIX = 1, // expr++ expr-- () [] . ?. - UNARY_PREFIX = 2, // -expr !expr ~expr ++expr --expr - MULTIPLICATIVE = 3, // * / % ~/ - ADDITIVE = 4, // + - - SHIFT = 5, // << >> - BITWISE_AND = 6, // & - BITWISE_XOR = 7, // ^ - BITWISE_OR = 8, // | - RELATIONAL = 9, // >= > <= < as is is! - EQUALITY = 10, // == != - LOGICAL_AND = 11, // && - LOGICAL_OR = 12, // || - IF_NULL = 13, // ?? - CONDITIONAL = 14, // expr ? expr : expr - CASCADE = 15, // .. - ASSIGNMENT = 16, // = *= /= ~/= %= += -= <<= >>= &= ^= |= - NONE = 99, // (...) -} - -export declare const dartGenerator: any; - -export {DartGenerator} from './generators/dart'; +export * from './generators/dart'; diff --git a/typings/javascript.d.ts b/typings/javascript.d.ts index ed1106bbc..e5558381d 100644 --- a/typings/javascript.d.ts +++ b/typings/javascript.d.ts @@ -4,44 +4,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export enum Order { - ATOMIC = 0, // 0 "" ... - NEW = 1.1, // new - MEMBER = 1.2, // . [] - FUNCTION_CALL = 2, // () - INCREMENT = 3, // ++ - DECREMENT = 3, // -- - BITWISE_NOT = 4.1, // ~ - UNARY_PLUS = 4.2, // + - UNARY_NEGATION = 4.3, // - - LOGICAL_NOT = 4.4, // ! - TYPEOF = 4.5, // typeof - VOID = 4.6, // void - DELETE = 4.7, // delete - AWAIT = 4.8, // await - EXPONENTIATION = 5.0, // ** - MULTIPLICATION = 5.1, // * - DIVISION = 5.2, // / - MODULUS = 5.3, // % - SUBTRACTION = 6.1, // - - ADDITION = 6.2, // + - BITWISE_SHIFT = 7, // << >> >>> - RELATIONAL = 8, // < <= > >= - IN = 8, // in - INSTANCEOF = 8, // instanceof - EQUALITY = 9, // == != === !== - BITWISE_AND = 10, // & - BITWISE_XOR = 11, // ^ - BITWISE_OR = 12, // | - LOGICAL_AND = 13, // && - LOGICAL_OR = 14, // || - CONDITIONAL = 15, // ?: - ASSIGNMENT = 16, // = += -= **= *= /= %= <<= >>= ... - YIELD = 17, // yield - COMMA = 18, // , - NONE = 99, // (...) -} - -export declare const javascriptGenerator: any; - -export {JavascriptGenerator} from './generators/javascript'; +export * from './generators/javascript'; diff --git a/typings/lua.d.ts b/typings/lua.d.ts index 752e8521a..6443fe90b 100644 --- a/typings/lua.d.ts +++ b/typings/lua.d.ts @@ -4,21 +4,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export enum Order { - ATOMIC = 0, // literals - // The next level was not explicit in documentation and inferred by Ellen. - HIGH = 1, // Function calls, tables[] - EXPONENTIATION = 2, // ^ - UNARY = 3, // not # - ~ - MULTIPLICATIVE = 4, // * / % - ADDITIVE = 5, // + - - CONCATENATION = 6, // .. - RELATIONAL = 7, // < > <= >= ~= == - AND = 8, // and - OR = 9, // or - NONE = 99, -} - -export declare const luaGenerator: any; - -export {LuaGenerator} from './generators/lua'; +export * from './generators/lua'; diff --git a/typings/msg/yue.d.ts b/typings/msg/ce.d.ts similarity index 74% rename from typings/msg/yue.d.ts rename to typings/msg/ce.d.ts index 5f2d13710..b6e6cc1d6 100644 --- a/typings/msg/yue.d.ts +++ b/typings/msg/ce.d.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * SPDX-License-Identifier: Apache-2.0 */ diff --git a/typings/msg/constants.d.ts b/typings/msg/constants.d.ts deleted file mode 100644 index 8908bc936..000000000 --- a/typings/msg/constants.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Type definitions for the Blockly constants locale. - */ - -/// - -import BlocklyMsg = Blockly.Msg; -export = BlocklyMsg; diff --git a/typings/msg/dtp.d.ts b/typings/msg/dtp.d.ts new file mode 100644 index 000000000..b6e6cc1d6 --- /dev/null +++ b/typings/msg/dtp.d.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './msg'; + diff --git a/typings/msg/hsb.d.ts b/typings/msg/hsb.d.ts new file mode 100644 index 000000000..b6e6cc1d6 --- /dev/null +++ b/typings/msg/hsb.d.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './msg'; + diff --git a/typings/msg/qqq.d.ts b/typings/msg/qqq.d.ts deleted file mode 100644 index 7880cc983..000000000 --- a/typings/msg/qqq.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Type definitions for the Blockly qqq locale. - */ - -/// - -import BlocklyMsg = Blockly.Msg; -export = BlocklyMsg; diff --git a/typings/msg/synonyms.d.ts b/typings/msg/synonyms.d.ts deleted file mode 100644 index f2e0dfc76..000000000 --- a/typings/msg/synonyms.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Type definitions for the Blockly synonyms locale. - */ - -/// - -import BlocklyMsg = Blockly.Msg; -export = BlocklyMsg; diff --git a/typings/msg/tdd.d.ts b/typings/msg/tdd.d.ts new file mode 100644 index 000000000..b6e6cc1d6 --- /dev/null +++ b/typings/msg/tdd.d.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './msg'; + diff --git a/typings/php.d.ts b/typings/php.d.ts index deaf9b899..96810bc30 100644 --- a/typings/php.d.ts +++ b/typings/php.d.ts @@ -4,46 +4,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export enum Order { - ATOMIC = 0, // 0 "" ... - CLONE = 1, // clone - NEW = 1, // new - MEMBER = 2.1, // [] - FUNCTION_CALL = 2.2, // () - POWER = 3, // ** - INCREMENT = 4, // ++ - DECREMENT = 4, // -- - BITWISE_NOT = 4, // ~ - CAST = 4, // (int) (float) (string) (array) ... - SUPPRESS_ERROR = 4, // @ - INSTANCEOF = 5, // instanceof - LOGICAL_NOT = 6, // ! - UNARY_PLUS = 7.1, // + - UNARY_NEGATION = 7.2, // - - MULTIPLICATION = 8.1, // * - DIVISION = 8.2, // / - MODULUS = 8.3, // % - ADDITION = 9.1, // + - SUBTRACTION = 9.2, // - - STRING_CONCAT = 9.3, // . - BITWISE_SHIFT = 10, // << >> - RELATIONAL = 11, // < <= > >= - EQUALITY = 12, // == != === !== <> <=> - REFERENCE = 13, // & - BITWISE_AND = 13, // & - BITWISE_XOR = 14, // ^ - BITWISE_OR = 15, // | - LOGICAL_AND = 16, // && - LOGICAL_OR = 17, // || - IF_NULL = 18, // ?? - CONDITIONAL = 19, // ?: - ASSIGNMENT = 20, // = += -= *= /= %= <<= >>= ... - LOGICAL_AND_WEAK = 21, // and - LOGICAL_XOR = 22, // xor - LOGICAL_OR_WEAK = 23, // or - NONE = 99, // (...) -} - -export declare const phpGenerator: any; - -export {PhpGenerator} from './generators/php'; +export * from './generators/php'; diff --git a/typings/python.d.ts b/typings/python.d.ts index c0a2c284b..fd1e3c677 100644 --- a/typings/python.d.ts +++ b/typings/python.d.ts @@ -4,30 +4,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export enum Order { - ATOMIC = 0, // 0 "" ... - COLLECTION = 1, // tuples, lists, dictionaries - STRING_CONVERSION = 1, // `expression...` - MEMBER = 2.1, // . [] - FUNCTION_CALL = 2.2, // () - EXPONENTIATION = 3, // ** - UNARY_SIGN = 4, // + - - BITWISE_NOT = 4, // ~ - MULTIPLICATIVE = 5, // * / // % - ADDITIVE = 6, // + - - BITWISE_SHIFT = 7, // << >> - BITWISE_AND = 8, // & - BITWISE_XOR = 9, // ^ - BITWISE_OR = 10, // | - RELATIONAL = 11, // in, not in, is, is not, >, >=, <>, !=, == - LOGICAL_NOT = 12, // not - LOGICAL_AND = 13, // and - LOGICAL_OR = 14, // or - CONDITIONAL = 15, // if else - LAMBDA = 16, // lambda - NONE = 99, // (...) -} - -export declare const pythonGenerator: any; - -export {PythonGenerator} from './generators/python'; +export * from './generators/python';