diff --git a/.github/workflows/appengine_deploy.yml b/.github/workflows/appengine_deploy.yml index 72cee90bf18..f7098d53a7e 100644 --- a/.github/workflows/appengine_deploy.yml +++ b/.github/workflows/appengine_deploy.yml @@ -42,7 +42,7 @@ jobs: path: _deploy/ - name: Deploy to App Engine - uses: google-github-actions/deploy-appengine@v2.0.0 + uses: google-github-actions/deploy-appengine@v2.1.0 # For parameters see: # https://github.com/google-github-actions/deploy-appengine#inputs with: diff --git a/appengine/add_timestamps.py b/appengine/add_timestamps.py index af877f3d6e9..05ff7b92814 100644 --- a/appengine/add_timestamps.py +++ b/appengine/add_timestamps.py @@ -62,8 +62,8 @@ def run_query(): while more: results, cursor, more = query.fetch_page(PAGE_SIZE, start_cursor=cursor) handle_results(results) - page_count = page_count + 1 - result_count = result_count + len(results) + page_count += 1 + result_count += len(results) print(f'{datetime.datetime.now().strftime("%I:%M:%S %p")} : page {page_count} : {result_count}') run_query() diff --git a/blocks/blocks.ts b/blocks/blocks.ts index 1a35c01305d..a9874e54df1 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,8 +17,8 @@ import * as variablesDynamic from './variables_dynamic.js'; import type {BlockDefinition} from '../core/blocks.js'; export { - colour, lists, + logic, loops, math, procedures, @@ -34,12 +33,12 @@ export { */ export const blocks: {[key: string]: BlockDefinition} = Object.assign( {}, - colour.blocks, lists.blocks, logic.blocks, loops.blocks, math.blocks, procedures.blocks, + texts.blocks, variables.blocks, variablesDynamic.blocks, ); diff --git a/blocks/colour.ts b/blocks/colour.ts deleted file mode 100644 index e57e4ba9b1e..00000000000 --- 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/lists.ts b/blocks/lists.ts index d115a321cb9..3b468dc9d21 100644 --- a/blocks/lists.ts +++ b/blocks/lists.ts @@ -227,7 +227,7 @@ const LISTS_CREATE_WITH = { // Disconnect any children that don't belong. for (let i = 0; i < this.itemCount_; i++) { const connection = this.getInput('ADD' + i)!.connection!.targetConnection; - if (connection && connections.indexOf(connection) === -1) { + if (connection && !connections.includes(connection)) { connection.disconnect(); } } diff --git a/blocks/loops.ts b/blocks/loops.ts index 02d9d34be72..c7cb710d770 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 7150bda8a29..1214eb55eda 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. @@ -858,7 +866,7 @@ const PROCEDURE_CALL_COMMON = { if ( mutatorOpen && connection && - paramIds.indexOf(this.quarkIds_[i]) === -1 + !paramIds.includes(this.quarkIds_[i]) ) { // This connection should no longer be attached to this block. connection.disconnect(); @@ -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')) { @@ -1043,7 +1049,7 @@ const PROCEDURE_CALL_COMMON = { } if ( event.type === Events.BLOCK_CREATE && - (event as BlockCreate).ids!.indexOf(this.id) !== -1 + (event as BlockCreate).ids!.includes(this.id) ) { // Look for the case where a procedure call was created (usually through // paste) and there is no matching definition. In this case, create @@ -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. } @@ -1288,7 +1302,7 @@ const PROCEDURES_IFRETURN = { // Is the block nested in a procedure? let block = this; // eslint-disable-line @typescript-eslint/no-this-alias do { - if (this.FUNCTION_TYPES.indexOf(block.type) !== -1) { + if (this.FUNCTION_TYPES.includes(block.type)) { legal = true; break; } @@ -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 824da782b72..91a27005a92 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': '', @@ -829,7 +796,7 @@ const JOIN_MUTATOR_MIXIN = { // Disconnect any children that don't belong. for (let i = 0; i < this.itemCount_; i++) { const connection = this.getInput('ADD' + i)!.connection!.targetConnection; - if (connection && connections.indexOf(connection) === -1) { + if (connection && !connections.includes(connection)) { connection.disconnect(); } } diff --git a/core/block.ts b/core/block.ts index 8f64b20a270..fa00c3ab4d3 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 @@ -671,6 +675,17 @@ export class Block implements IASTNodeLocation, IDeletable { return block; } + /** + * Returns this block if it is a shadow block, or the first non-shadow parent. + * + * @internal + */ + getFirstNonShadowBlock(): this { + if (!this.isShadow()) return this; + // We can assert the parent is non-null because shadows must have parents. + return this.getParent()!.getFirstNonShadowBlock(); + } + /** * Find all the blocks that are directly nested inside this one. * Includes value and statement inputs, as well as any following statement. @@ -1388,32 +1403,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 +1498,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 +1507,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. * @@ -1526,8 +1619,7 @@ export class Block implements IASTNodeLocation, IDeletable { checks = connection.targetConnection.getCheck(); } return ( - !!checks && - (checks.indexOf('Boolean') !== -1 || checks.indexOf('Number') !== -1) + !!checks && (checks.includes('Boolean') || checks.includes('Number')) ); } @@ -2209,7 +2301,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; } @@ -2219,19 +2311,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_animations.ts b/core/block_animations.ts index 615aa81d114..f3fc3d454f0 100644 --- a/core/block_animations.ts +++ b/core/block_animations.ts @@ -38,18 +38,22 @@ export function disposeUiEffect(block: BlockSvg) { const svgGroup = block.getSvgRoot(); workspace.getAudioManager().play('delete'); - const xy = workspace.getSvgXY(svgGroup); + const xy = block.getRelativeToSurfaceXY(); // Deeply clone the current block. const clone: SVGGElement = svgGroup.cloneNode(true) as SVGGElement; clone.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')'); - workspace.getParentSvg().appendChild(clone); + workspace.getLayerManager()?.appendToAnimationLayer({ + getSvgRoot: () => { + return clone; + }, + }); const cloneRect = { 'x': xy.x, 'y': xy.y, 'width': block.width, 'height': block.height, }; - disposeUiStep(clone, cloneRect, workspace.RTL, new Date(), workspace.scale); + disposeUiStep(clone, cloneRect, workspace.RTL, new Date()); } /** * Animate a cloned block and eventually dispose of it. @@ -60,29 +64,26 @@ export function disposeUiEffect(block: BlockSvg) { * @param rect Starting rect of the clone. * @param rtl True if RTL, false if LTR. * @param start Date of animation's start. - * @param workspaceScale Scale of workspace. */ function disposeUiStep( clone: Element, rect: CloneRect, rtl: boolean, start: Date, - workspaceScale: number, ) { const ms = new Date().getTime() - start.getTime(); const percent = ms / 150; if (percent > 1) { dom.removeNode(clone); } else { - const x = - rect.x + (((rtl ? -1 : 1) * rect.width * workspaceScale) / 2) * percent; - const y = rect.y + rect.height * workspaceScale * percent; - const scale = (1 - percent) * workspaceScale; + const x = rect.x + (((rtl ? -1 : 1) * rect.width) / 2) * percent; + const y = rect.y + (rect.height / 2) * percent; + const scale = 1 - percent; clone.setAttribute( 'transform', 'translate(' + x + ',' + y + ')' + ' scale(' + scale + ')', ); - setTimeout(disposeUiStep, 10, clone, rect, rtl, start, workspaceScale); + setTimeout(disposeUiStep, 10, clone, rect, rtl, start); } } diff --git a/core/block_dragger.ts b/core/block_dragger.ts deleted file mode 100644 index 8d57344ea2e..00000000000 --- a/core/block_dragger.ts +++ /dev/null @@ -1,672 +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; - - /** - * @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.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 (delta) { - // !preventMove - this.updateBlockAfterMove_(); - } else { - // 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_, - ); - } - } - // Must dispose after `updateBlockAfterMove_` is called to not break the - // dynamic connections plugin. - this.connectionPreviewer.dispose(); - this.workspace_.setResizesEnabled(true); - - eventUtils.setGroup(false); - } - - /** - * 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 f8f0c02ff76..ca2ad181a82 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()) { @@ -212,9 +208,9 @@ export class BlockSvg icon.updateEditable(); } this.applyColour(); - this.pathObject.updateMovable(this.isMovable()); + this.pathObject.updateMovable(this.isMovable() || this.isInFlyout); 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(); - return; - } - if (common.getSelected() === this) { + if (this.isShadow()) { + this.getParent()?.select(); 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 92d4a674010..cee509480c2 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,17 +104,14 @@ 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 './connection_previewers/insertion_marker_previewer.js'; +import {InsertionMarkerPreviewer} from './insertion_marker_previewer.js'; 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 = []; - - if (this.isDeletable() && this.isMovable()) { - menuOptions.push(ContextMenu.commentDuplicateOption(this)); - menuOptions.push(ContextMenu.commentDeleteOption(this)); - } +WorkspaceSvg.prototype.newComment = function ( + id?: string, +): comments.RenderedWorkspaceComment { + return new comments.RenderedWorkspaceComment(this, id); +}; - 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 e494c7ad2dc..00000000000 --- 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 4bea9d863b7..35b9e7dde0a 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 f459654fac8..74317d57bc1 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 6f50d303b0e..020ab4f2ec1 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 081f86097b6..d7d1f5ae7db 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); @@ -142,7 +143,7 @@ export class TextInputBubble extends Bubble { /** Binds events to the text area element. */ private bindTextAreaEvents(textArea: HTMLTextAreaElement) { - // Don't zoom with mousewheel. + // Don't zoom with mousewheel; let it scroll instead. browserEvents.conditionalBind(textArea, 'wheel', this, (e: Event) => { e.stopPropagation(); }); @@ -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; } /** @@ -221,19 +198,12 @@ export class TextInputBubble extends Bubble { const heightMinusBorder = size.height - Bubble.DOUBLE_BORDER; this.inputRoot.setAttribute('width', `${widthMinusBorder}`); this.inputRoot.setAttribute('height', `${heightMinusBorder}`); - this.textArea.style.width = `${widthMinusBorder - 4}px`; - this.textArea.style.height = `${heightMinusBorder - 4}px`; + this.resizeGroup.setAttribute('y', `${heightMinusBorder}`); if (this.workspace.RTL) { - this.resizeGroup.setAttribute( - 'transform', - `translate(${Bubble.DOUBLE_BORDER}, ${heightMinusBorder}) scale(-1 1)`, - ); + this.resizeGroup.setAttribute('x', `${-Bubble.DOUBLE_BORDER}`); } else { - this.resizeGroup.setAttribute( - 'transform', - `translate(${widthMinusBorder}, ${heightMinusBorder})`, - ); + this.resizeGroup.setAttribute('x', `${widthMinusBorder}`); } super.setSize(size, relayout); @@ -330,14 +300,15 @@ export class TextInputBubble extends Bubble { } Css.register(` -.blocklyCommentTextarea { - background-color: #fef49c; +.blocklyTextInputBubble .blocklyTextarea { + background-color: var(--commentFillColour); border: 0; + box-sizing: border-box; display: block; - margin: 0; outline: 0; - padding: 3px; + padding: 5px; resize: none; - text-overflow: hidden; + width: 100%; + height: 100%; } `); diff --git a/core/bump_objects.ts b/core/bump_objects.ts index 39fcd0a505b..3ceae2dbcfd 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'; /** @@ -99,7 +98,7 @@ export function bumpIntoBoundsHandler( return; } - if (eventUtils.BUMP_EVENTS.indexOf(e.type ?? '') !== -1) { + if (eventUtils.BUMP_EVENTS.includes(e.type ?? '')) { const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true); // Triggered by move/create event @@ -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 2c3872c1ab9..ed574d11287 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 0bb707c3654..f82adf2b385 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'; @@ -29,11 +30,21 @@ export class BlockPaster implements IPaster { copyData.blockState['y'] = coordinate.y; } + // After appending the block to the workspace, it will be bumped from its neighbors + // However, the algorithm for deciding where to paste a block depends on + // the starting position of the copied block, so we'll pass those coordinates along + const initialCoordinates = + coordinate || + new Coordinate( + copyData.blockState['x'] || 0, + copyData.blockState['y'] || 0, + ); + eventUtils.disable(); let block; try { block = append(copyData.blockState, workspace) as BlockSvg; - moveBlockToNotConflict(block); + moveBlockToNotConflict(block, initialCoordinates); } finally { eventUtils.enable(); } @@ -43,7 +54,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; } } @@ -55,12 +66,20 @@ export class BlockPaster implements IPaster { * Exported for testing. * * @param block The block to move to an unambiguous location. + * @param originalPosition The initial coordinate to start searching from, + * likely the position of the copied block. * @internal */ -export function moveBlockToNotConflict(block: BlockSvg) { +export function moveBlockToNotConflict( + block: BlockSvg, + originalPosition: Coordinate, +) { const workspace = block.workspace; const snapRadius = config.snapRadius; - const coord = block.getRelativeToSurfaceXY(); + const bumpOffset = Coordinate.difference( + originalPosition, + block.getRelativeToSurfaceXY(), + ); const offset = new Coordinate(0, 0); // getRelativeToSurfaceXY is really expensive, so we want to cache this. const otherCoords = workspace @@ -69,8 +88,11 @@ export function moveBlockToNotConflict(block: BlockSvg) { .map((b) => b.getRelativeToSurfaceXY()); while ( - blockOverlapsOtherExactly(Coordinate.sum(coord, offset), otherCoords) || - blockIsInSnapRadius(block, offset, snapRadius) + blockOverlapsOtherExactly( + Coordinate.sum(originalPosition, offset), + otherCoords, + ) || + blockIsInSnapRadius(block, Coordinate.sum(bumpOffset, offset), snapRadius) ) { if (workspace.RTL) { offset.translate(-snapRadius, snapRadius * 2); @@ -79,7 +101,7 @@ export function moveBlockToNotConflict(block: BlockSvg) { } } - block!.moveTo(Coordinate.sum(coord, offset)); + block!.moveTo(Coordinate.sum(originalPosition, offset)); } /** diff --git a/core/clipboard/workspace_comment_paster.ts b/core/clipboard/workspace_comment_paster.ts index aeedbfb2b77..c7e5eed68b0 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; + } + + eventUtils.disable(); + let comment; + try { + comment = commentSerialiation.append( + state, + workspace, + ) as RenderedWorkspaceComment; + moveCommentToNotConflict(comment); + } finally { + eventUtils.enable(); } - return WorkspaceCommentSvg.fromXmlRendered( - copyData.commentState, - workspace, + + 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 00000000000..368db0e7783 --- /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 00000000000..52f92b56776 --- /dev/null +++ b/core/comments/comment_view.ts @@ -0,0 +1,873 @@ +/** + * @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. + * Respects collapsing. + */ + getSize(): Size { + return this.collapsed ? this.topBarBackground.getBBox() : 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}`); + } + } + + /** + * 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); + box-sizing: border-box; + display: block; + outline: 0; + padding: 5px; + resize: none; + width: 100%; + height: 100%; +} + +.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 { + cursor: grab; + 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 00000000000..b5818895f59 --- /dev/null +++ b/core/comments/rendered_workspace_comment.ts @@ -0,0 +1,291 @@ +/** + * @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, + ); + // Don't zoom with mousewheel; let it scroll instead. + browserEvents.conditionalBind( + this.view.getSvgRoot(), + 'wheel', + this, + (e: Event) => { + e.stopPropagation(); + }, + ); + } + + /** + * 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 comment's size in workspace units. + * Does not respect collapsing. + */ + getSize(): Size { + return super.getSize(); + } + + /** + * Returns the bounding rectangle of this comment in workspace coordinates. + * Respects collapsing. + */ + getBoundingRectangle(): Rect { + const loc = this.getRelativeToSurfaceXY(); + const size = this.view?.getSize() ?? this.getSize(); + let left; + let right; + if (this.workspace.RTL) { + left = loc.x - size.width; + right = loc.x; + } else { + left = loc.x; + right = loc.x + size.width; + } + return new Rect(loc.y, loc.y + size.height, left, right); + } + + /** 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); + } + + /** + * 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.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 00000000000..3c23aba86a2 --- /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 29fb7777b8b..fba960a5b5a 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(); } /** @@ -282,7 +294,9 @@ export function defineBlocks(blocks: {[key: string]: BlockDefinition}) { for (const type of Object.keys(blocks)) { const definition = blocks[type]; if (type in Blocks) { - console.warn(`Block definiton "${type}" overwrites previous definition.`); + console.warn( + `Block definition "${type}" overwrites previous definition.`, + ); } Blocks[type] = definition; } diff --git a/core/component_manager.ts b/core/component_manager.ts index 9bb66fcbbf4..b9654c94ec5 100644 --- a/core/component_manager.ts +++ b/core/component_manager.ts @@ -172,7 +172,7 @@ export class ComponentManager { capability = `${capability}`.toLowerCase(); return ( this.componentData.has(id) && - this.componentData.get(id)!.capabilities.indexOf(capability) !== -1 + this.componentData.get(id)!.capabilities.includes(capability) ); } diff --git a/core/config.ts b/core/config.ts index a6642c266c9..9def1dca4e9 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 56ba4c3a526..1dd8dc1ea55 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()) { @@ -222,7 +224,7 @@ export class Connection implements IASTNodeLocationWithBlock { * Connect this connection to another connection. * * @param otherConnection Connection to connect to. - * @returns Whether the the blocks are now connected or not. + * @returns Whether the blocks are now connected or not. */ connect(otherConnection: Connection): boolean { if (this.targetConnection === otherConnection) { @@ -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/connection_checker.ts b/core/connection_checker.ts index f7d09b97222..6f5ecd5d5c1 100644 --- a/core/connection_checker.ts +++ b/core/connection_checker.ts @@ -210,7 +210,7 @@ export class ConnectionChecker implements IConnectionChecker { } // Find any intersection in the check lists. for (let i = 0; i < checkArrayOne.length; i++) { - if (checkArrayTwo.indexOf(checkArrayOne[i]) !== -1) { + if (checkArrayTwo.includes(checkArrayOne[i])) { return true; } } @@ -298,7 +298,7 @@ export class ConnectionChecker implements IConnectionChecker { } // Don't let blocks try to connect to themselves or ones they nest. - if (common.draggingConnections.indexOf(b) !== -1) { + if (common.draggingConnections.includes(b)) { return false; } @@ -321,7 +321,7 @@ export class ConnectionChecker implements IConnectionChecker { } // Don't let blocks try to connect to themselves or ones they nest. - if (common.draggingConnections.indexOf(b) !== -1) { + if (common.draggingConnections.includes(b)) { return false; } diff --git a/core/constants.ts b/core/constants.ts index 7c3312f29da..538bd378300 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 78431c93f3a..939477b3c4f 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 a540a1341ce..254906ce7ff 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 c1183ca9723..abbd0f9756d 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 07e9c98a485..5a44cc0b603 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 @@ -497,7 +500,7 @@ input[type=number] { margin-right: -24px; } -.blocklyBlockDragSurface { +.blocklyBlockDragSurface, .blocklyAnimationLayer { position: absolute; top: 0; left: 0; diff --git a/core/delete_area.ts b/core/delete_area.ts index ca47c1a9ed4..4967927c42c 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 3f58bdc564c..e973f2dd1c3 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 00000000000..a7e46fc27da --- /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 00000000000..fb913f88215 --- /dev/null +++ b/core/dragging/block_drag_strategy.ts @@ -0,0 +1,408 @@ +/** + * @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; + + constructor(private block: BlockSvg) { + this.workspace = block.workspace; + } + + /** Returns true if the block is currently movable. False otherwise. */ + isMovable(): boolean { + return ( + this.block.isOwnMovable() && + !this.block.isShadow() && + !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 { + 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); + } + + /** + * 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 { + 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(): void { + 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 { + 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 00000000000..7ffccddc1e5 --- /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 00000000000..804ad2e9c9e --- /dev/null +++ b/core/dragging/comment_drag_strategy.ts @@ -0,0 +1,70 @@ +/** + * @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 + .getLayerManager() + ?.moveOffDragLayer(this.comment, layers.BLOCK); + this.comment.setDragging(false); + + this.comment.snapToGrid(); + + this.workspace.setResizesEnabled(true); + eventUtils.setGroup(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 00000000000..71cfae471e4 --- /dev/null +++ b/core/dragging/dragger.ts @@ -0,0 +1,157 @@ +/** + * @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'; +import * as eventUtils from '../events/utils.js'; +import * as blockAnimations from '../block_animations.js'; +import {BlockSvg} from '../block_svg.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 origGroup = eventUtils.getGroup(); + const dragTarget = this.workspace.getDragTarget(e); + if (dragTarget) { + this.dragTarget?.onDrop(this.draggable); + } + + if (this.shouldReturnToStart(e, this.draggable)) { + this.draggable.revertDrag(); + } + + const wouldDelete = + isDeletable(this.draggable) && + this.wouldDeleteDraggable(e, this.draggable); + + // TODO(#8148): use a generalized API instead of an instanceof check. + if (wouldDelete && this.draggable instanceof BlockSvg) { + blockAnimations.disposeUiEffect(this.draggable); + } + + this.draggable.endDrag(e); + + if (wouldDelete && isDeletable(this.draggable)) { + // We want to make sure the delete gets grouped with any possible + // move event. + const newGroup = eventUtils.getGroup(); + eventUtils.setGroup(origGroup); + this.draggable.dispose(); + eventUtils.setGroup(newGroup); + } + } + + /** + * 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 6f16387fde0..bb8011755fc 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 3ef5c10850f..570b7f58e2c 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_click.ts b/core/events/events_click.ts index 4f2c0c53e70..1b15560695d 100644 --- a/core/events/events_click.ts +++ b/core/events/events_click.ts @@ -20,7 +20,7 @@ import * as eventUtils from './utils.js'; import {Workspace} from '../workspace.js'; /** - * Notifies listeners that ome blockly element was clicked. + * Notifies listeners that some blockly element was clicked. */ export class Click extends UiBase { /** The ID of the block that was clicked, if a block was clicked. */ diff --git a/core/events/events_comment_base.ts b/core/events/events_comment_base.ts index fe60d0d92ee..6fbc95c4d60 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 be0e285a59e..eb39d929de9 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 00000000000..6646b1df296 --- /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 4db859c7b5b..692397df683 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 ee08c602cca..62f8916fb3b 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 013064a5dac..502ca032f4c 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 469f105824c..eacf0490673 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/extensions.ts b/core/extensions.ts index 6313c416161..aa9a6a1860d 100644 --- a/core/extensions.ts +++ b/core/extensions.ts @@ -401,7 +401,7 @@ export function buildTooltipForDropdown( const blockTypesChecked: string[] = []; return function (this: Block) { - if (blockTypesChecked.indexOf(this.type) === -1) { + if (!blockTypesChecked.includes(this.type)) { checkDropdownOptionsInTable(this, dropdownName, lookupTable); blockTypesChecked.push(this.type); } diff --git a/core/field.ts b/core/field.ts index 7522cde9336..51e006823d7 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(); } } @@ -1086,14 +1086,22 @@ export abstract class Field } const classValidation = this.doClassValidation_(newValue); - const classValue = this.processValidation_(newValue, classValidation); + const classValue = this.processValidation_( + newValue, + classValidation, + fireChangeEvent, + ); if (classValue instanceof Error) { doLogging && console.log('invalid class validation, return'); return; } const localValidation = this.getValidator()?.call(this, classValue); - const localValue = this.processValidation_(classValue, localValidation); + const localValue = this.processValidation_( + classValue, + localValidation, + fireChangeEvent, + ); if (localValue instanceof Error) { doLogging && console.log('invalid local validation, return'); return; @@ -1135,14 +1143,16 @@ export abstract class Field * * @param newValue New value. * @param validatedValue Validated value. + * @param fireChangeEvent Whether to fire a change event if the value changes. * @returns New value, or an Error object. */ private processValidation_( newValue: AnyDuringMigration, validatedValue: T | null | undefined, + fireChangeEvent: boolean, ): T | Error { if (validatedValue === null) { - this.doValueInvalid_(newValue); + this.doValueInvalid_(newValue, fireChangeEvent); if (this.isDirty_) { this.forceRerender(); } @@ -1209,8 +1219,12 @@ export abstract class Field * No-op by default. * * @param _invalidValue The input value that was determined to be invalid. + * @param _fireChangeEvent Whether to fire a change event if the value changes. */ - protected doValueInvalid_(_invalidValue: AnyDuringMigration) {} + protected doValueInvalid_( + _invalidValue: AnyDuringMigration, + _fireChangeEvent: boolean = true, + ) {} // NOP /** @@ -1419,6 +1433,22 @@ export abstract class Field workspace.getMarker(MarkerManager.LOCAL_MARKER)!.draw(); } } + + /** + * Subclasses should reimplement this method to construct their Field + * subclass from a JSON arg object. + * + * It is an error to attempt to register a field subclass in the + * FieldRegistry if that subclass has not overridden this method. + * + * @param _options JSON configuration object with properties needed + * to configure a specific field. + */ + static fromJson(_options: FieldConfig): Field { + throw new Error( + `Attempted to instantiate a field from the registry that hasn't defined a 'fromJson' method.`, + ); + } } /** @@ -1429,12 +1459,14 @@ export interface FieldConfig { } /** - * For use by Field and descendants of Field. Constructors can change + * Represents an object that has all the prototype properties of the `Field` + * class. This is necessary because constructors can change * in descendants, though they should contain all of Field's prototype methods. * - * @internal + * This type should only be used in places where we directly access the prototype + * of a Field class or subclass. */ -export type FieldProto = Pick; +type FieldProto = Pick; /** * Represents an error where the field is trying to access its block or diff --git a/core/field_angle.ts b/core/field_angle.ts deleted file mode 100644 index e65ef16a956..00000000000 --- 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, 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 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_checkbox.ts b/core/field_checkbox.ts index 7bda3a58f35..83f460bb9d2 100644 --- a/core/field_checkbox.ts +++ b/core/field_checkbox.ts @@ -226,7 +226,9 @@ export class FieldCheckbox extends Field { * @nocollapse * @internal */ - static fromJson(options: FieldCheckboxFromJsonConfig): FieldCheckbox { + static override fromJson( + options: FieldCheckboxFromJsonConfig, + ): FieldCheckbox { // `this` might be a subclass of FieldCheckbox if that class doesn't // 'override' the static fromJson method. return new this(options.checked, undefined, options); diff --git a/core/field_colour.ts b/core/field_colour.ts deleted file mode 100644 index 6b78a2e5088..00000000000 --- 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 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_dropdown.ts b/core/field_dropdown.ts index ad728f2267a..58a4b073218 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -229,6 +229,9 @@ export class FieldDropdown extends Field { : ' ' + FieldDropdown.ARROW_CHAR, ), ); + if (this.getConstants()!.FIELD_TEXT_BASELINE_CENTER) { + this.arrow.setAttribute('dominant-baseline', 'central'); + } if (this.getSourceBlock()?.RTL) { this.getTextElement().insertBefore(this.arrow, this.textContent_); } else { @@ -408,7 +411,13 @@ export class FieldDropdown extends Field { * @param newValue The input value. * @returns A valid language-neutral option, or null if invalid. */ - protected override doClassValidation_(newValue?: string): string | null { + protected override doClassValidation_( + newValue: string, + ): string | null | undefined; + protected override doClassValidation_(newValue?: string): string | null; + protected override doClassValidation_( + newValue?: string, + ): string | null | undefined { const options = this.getOptions(true); const isValueValid = options.some((option) => option[1] === newValue); @@ -426,7 +435,7 @@ export class FieldDropdown extends Field { } return null; } - return newValue as string; + return newValue; } /** @@ -638,7 +647,9 @@ export class FieldDropdown extends Field { * @nocollapse * @internal */ - static fromJson(options: FieldDropdownFromJsonConfig): FieldDropdown { + static override fromJson( + options: FieldDropdownFromJsonConfig, + ): FieldDropdown { if (!options.options) { throw new Error( 'options are required for the dropdown field. The ' + diff --git a/core/field_image.ts b/core/field_image.ts index 6d31a9772ad..6e83e3405c6 100644 --- a/core/field_image.ts +++ b/core/field_image.ts @@ -250,7 +250,7 @@ export class FieldImage extends Field { * @nocollapse * @internal */ - static fromJson(options: FieldImageFromJsonConfig): FieldImage { + static override fromJson(options: FieldImageFromJsonConfig): FieldImage { if (!options.src || !options.width || !options.height) { throw new Error( 'src, width, and height values for an image field are' + @@ -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_input.ts b/core/field_input.ts index 51304705427..5c26f42b384 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -166,17 +166,26 @@ export abstract class FieldInput extends Field< * value while allowing the display text to be handled by the htmlInput_. * * @param _invalidValue The input value that was determined to be invalid. - * This is not used by the text input because its display value is stored - * on the htmlInput_. + * This is not used by the text input because its display value is stored + * on the htmlInput_. + * @param fireChangeEvent Whether to fire a change event if the value changes. */ - protected override doValueInvalid_(_invalidValue: AnyDuringMigration) { + protected override doValueInvalid_( + _invalidValue: AnyDuringMigration, + fireChangeEvent: boolean = true, + ) { if (this.isBeingEdited_) { this.isDirty_ = true; this.isTextValid_ = false; const oldValue = this.value_; // Revert value when the text becomes invalid. - this.value_ = this.htmlInput_!.getAttribute('data-untyped-default-value'); - if (this.sourceBlock_ && eventUtils.isEnabled()) { + this.value_ = this.valueWhenEditorWasOpened_; + if ( + this.sourceBlock_ && + eventUtils.isEnabled() && + this.value_ !== oldValue && + fireChangeEvent + ) { eventUtils.fire( new (eventUtils.get(eventUtils.BLOCK_CHANGE))( this.sourceBlock_, @@ -566,7 +575,10 @@ export abstract class FieldInput extends Field< // intermediate changes that do not get recorded in undo history. const oldValue = this.value_; // Change the field's value without firing the normal change event. - this.setValue(this.getValueFromEditorText_(this.htmlInput_!.value), false); + this.setValue( + this.getValueFromEditorText_(this.htmlInput_!.value), + /* fireChangeEvent= */ false, + ); if ( this.sourceBlock_ && eventUtils.isEnabled() && diff --git a/core/field_label.ts b/core/field_label.ts index 7409ca59424..2b77b0d25ff 100644 --- a/core/field_label.ts +++ b/core/field_label.ts @@ -117,7 +117,7 @@ export class FieldLabel extends Field { * @nocollapse * @internal */ - static fromJson(options: FieldLabelFromJsonConfig): FieldLabel { + static override fromJson(options: FieldLabelFromJsonConfig): FieldLabel { const text = parsing.replaceMessageReferences(options.text); // `this` might be a subclass of FieldLabel if that class doesn't override // the static fromJson method. diff --git a/core/field_multilineinput.ts b/core/field_multilineinput.ts deleted file mode 100644 index b1ce0cb10c3..00000000000 --- 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/field_number.ts b/core/field_number.ts index 8965dd3300c..e8e51d06007 100644 --- a/core/field_number.ts +++ b/core/field_number.ts @@ -216,7 +216,7 @@ export class FieldNumber extends FieldInput { private setPrecisionInternal(precision: number | string | undefined | null) { this.precision_ = Number(precision) || 0; let precisionString = String(this.precision_); - if (precisionString.indexOf('e') !== -1) { + if (precisionString.includes('e')) { // String() is fast. But it turns .0000001 into '1e-7'. // Use the much slower toLocaleString to access all the digits. precisionString = this.precision_.toLocaleString('en-US', { @@ -315,7 +315,7 @@ export class FieldNumber extends FieldInput { * @nocollapse * @internal */ - static fromJson(options: FieldNumberFromJsonConfig): FieldNumber { + static override fromJson(options: FieldNumberFromJsonConfig): FieldNumber { // `this` might be a subclass of FieldNumber if that class doesn't override // the static fromJson method. return new this( diff --git a/core/field_registry.ts b/core/field_registry.ts index 0bd9e562687..06bb9acd045 100644 --- a/core/field_registry.ts +++ b/core/field_registry.ts @@ -6,12 +6,46 @@ // Former goog.module ID: Blockly.fieldRegistry -import type {Field, FieldProto} from './field.js'; +import type {Field, FieldConfig} from './field.js'; import * as registry from './registry.js'; -interface RegistryOptions { +/** + * When constructing a field from JSON using the registry, the + * `fromJson` method in this file is called with an options parameter + * object consisting of the "type" which is the name of the field, and + * other options that are part of the field's config object. + * + * These options are then passed to the field's static `fromJson` + * method. That method accepts an options parameter with a type that usually + * extends from FieldConfig, and may or may not have a "type" attribute (in + * fact, it shouldn't, because we'd overwrite it as described above!) + * + * Unfortunately the registry has no way of knowing the actual Field subclass + * that will be returned from passing in the name of the field. Therefore it + * also has no way of knowing that the options object not only implements + * `FieldConfig`, but it also should satisfy the Config that belongs to that + * specific class's `fromJson` method. + * + * Because of this uncertainty, we just give up on type checking the properties + * passed to the `fromJson` method, and allow arbitrary string keys with + * unknown types. + */ +type RegistryOptions = FieldConfig & { + // The name of the field, e.g. field_dropdown type: string; [key: string]: unknown; +}; + +/** + * Represents the static methods that must be defined on any + * field that is registered, i.e. the constructor and fromJson methods. + * + * Because we don't know which Field subclass will be registered, we + * are unable to typecheck the parameters of the constructor. + */ +export interface RegistrableField { + new (...args: any[]): Field; + fromJson(options: FieldConfig): Field; } /** @@ -25,7 +59,7 @@ interface RegistryOptions { * @throws {Error} if the type name is empty, the field is already registered, * or the fieldClass is not an object containing a fromJson function. */ -export function register(type: string, fieldClass: FieldProto) { +export function register(type: string, fieldClass: RegistrableField) { registry.register(registry.Type.FIELD, type, fieldClass); } @@ -59,7 +93,10 @@ export function fromJson(options: RegistryOptions): Field | null { * @param options */ function fromJsonInternal(options: RegistryOptions): Field | null { - const fieldObject = registry.getObject(registry.Type.FIELD, options.type); + const fieldObject = registry.getObject( + registry.Type.FIELD, + options.type, + ) as unknown as RegistrableField; if (!fieldObject) { console.warn( 'Blockly could not create a field of type ' + @@ -69,12 +106,8 @@ function fromJsonInternal(options: RegistryOptions): Field | null { ' #1584), or the registration is not being reached.', ); return null; - } else if (typeof (fieldObject as any).fromJson !== 'function') { - throw new TypeError('returned Field was not a IRegistrableField'); - } else { - type fromJson = (options: {}) => Field; - return (fieldObject as unknown as {fromJson: fromJson}).fromJson(options); } + return fieldObject.fromJson(options); } export const TEST_ONLY = { diff --git a/core/field_textinput.ts b/core/field_textinput.ts index 39e82b5a4f7..39bdca97056 100644 --- a/core/field_textinput.ts +++ b/core/field_textinput.ts @@ -73,7 +73,9 @@ export class FieldTextInput extends FieldInput { * @nocollapse * @internal */ - static fromJson(options: FieldTextInputFromJsonConfig): FieldTextInput { + static override fromJson( + options: FieldTextInputFromJsonConfig, + ): FieldTextInput { const text = parsing.replaceMessageReferences(options.text); // `this` might be a subclass of FieldTextInput if that class doesn't // override the static fromJson method. diff --git a/core/field_variable.ts b/core/field_variable.ts index 95474e4b1cf..539557256b6 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -444,7 +444,6 @@ export class FieldVariable extends FieldDropdown { // #1499. // Set the allowable variable types. Null means all types on the workspace. if (Array.isArray(variableTypes)) { - variableTypes = variableTypes; // Make sure the default type is valid. let isInArray = false; for (let i = 0; i < variableTypes.length; i++) { diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 438e92d6430..f9e6545a957 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -13,7 +13,7 @@ import type {Abstract as AbstractEvent} from './events/events_abstract.js'; import type {Block} from './block.js'; -import type {BlockSvg} from './block_svg.js'; +import {BlockSvg} from './block_svg.js'; import * as browserEvents from './browser_events.js'; import * as common from './common.js'; import {ComponentManager} from './component_manager.js'; @@ -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. */ @@ -161,6 +169,11 @@ export abstract class Flyout */ protected buttons_: FlyoutButton[] = []; + /** + * List of visible buttons and blocks. + */ + protected contents: FlyoutItem[] = []; + /** * List of event listeners. */ @@ -546,6 +559,23 @@ export abstract class Flyout } } + /** + * Get the list of buttons and blocks of the current flyout. + * + * @returns The array of flyout buttons and blocks. + */ + getContents(): FlyoutItem[] { + return this.contents; + } + + /** + * Store the list of buttons and blocks on the flyout. + * + * @param contents - The array of items for the flyout. + */ + setContents(contents: FlyoutItem[]): void { + this.contents = contents; + } /** * Update the display property of the flyout based whether it thinks it should * be visible and whether its containing workspace is visible. @@ -651,6 +681,8 @@ export abstract class Flyout renderManagement.triggerQueuedRenders(this.workspace_); + this.setContents(flyoutInfo.contents); + this.layout_(flyoutInfo.contents, flyoutInfo.gaps); if (this.horizontalLayout) { @@ -804,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_, @@ -1201,12 +1239,15 @@ export abstract class Flyout private filterForCapacity() { const blocks = this.workspace_.getTopBlocks(false); for (let i = 0, block; (block = blocks[i]); i++) { - if (this.permanentlyDisabled.indexOf(block) === -1) { + if (!this.permanentlyDisabled.includes(block)) { const enable = this.targetWorkspace.isCapacityAvailable( common.getBlockTypeCounts(block), ); while (block) { - block.setEnabled(enable); + block.setDisabledReason( + !enable, + WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON, + ); block = block.getNextBlock(); } } @@ -1251,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; @@ -1261,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/flyout_button.ts b/core/flyout_button.ts index 498b9b1ada7..5b20d24da0e 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -20,11 +20,12 @@ import * as style from './utils/style.js'; import {Svg} from './utils/svg.js'; import type * as toolbox from './utils/toolbox.js'; import type {WorkspaceSvg} from './workspace_svg.js'; +import type {IASTNodeLocationSvg} from './blockly.js'; /** * Class for a button or label in the flyout. */ -export class FlyoutButton { +export class FlyoutButton implements IASTNodeLocationSvg { /** The horizontal margin around the text in the button. */ static TEXT_MARGIN_X = 5; @@ -55,6 +56,12 @@ export class FlyoutButton { /** The SVG element with the text of the label or button. */ private svgText: SVGTextElement | null = null; + /** + * Holds the cursors svg element when the cursor is attached to the button. + * This is null if there is no cursor on the button. + */ + cursorSvg: SVGElement | null = null; + /** * @param workspace The workspace in which to place this button. * @param targetWorkspace The flyout's target workspace. @@ -255,6 +262,15 @@ export class FlyoutButton { return this.targetWorkspace; } + /** + * Get the button's workspace. + * + * @returns The workspace in which to place this button. + */ + getWorkspace(): WorkspaceSvg { + return this.workspace; + } + /** Dispose of this button. */ dispose() { if (this.onMouseUpWrapper) { @@ -268,6 +284,32 @@ export class FlyoutButton { } } + /** + * Add the cursor SVG to this buttons's SVG group. + * + * @param cursorSvg The SVG root of the cursor to be added to the button SVG + * group. + */ + setCursorSvg(cursorSvg: SVGElement) { + if (!cursorSvg) { + this.cursorSvg = null; + return; + } + if (this.svgGroup) { + this.svgGroup.appendChild(cursorSvg); + this.cursorSvg = cursorSvg; + } + } + + /** + * Required by IASTNodeLocationSvg, but not used. A marker cannot be set on a + * button. If the 'mark' shortcut is used on a button, its associated callback + * function is triggered. + */ + setMarkerSvg() { + throw new Error('Attempted to set a marker on a button.'); + } + /** * Do something when the button is clicked. * diff --git a/core/generator.ts b/core/generator.ts index d94597fc127..7bb509f8ba3 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 ` + @@ -313,6 +303,9 @@ export class CodeGenerator { throw TypeError('Expecting valid order from block: ' + block.type); } const targetBlock = block.getInputTargetBlock(name); + if (!targetBlock && !block.getInput(name)) { + throw ReferenceError(`Input "${name}" doesn't exist on "${block.type}"`); + } if (!targetBlock) { return ''; } @@ -391,6 +384,9 @@ export class CodeGenerator { */ statementToCode(block: Block, name: string): string { const targetBlock = block.getInputTargetBlock(name); + if (!targetBlock && !block.getInput(name)) { + throw ReferenceError(`Input "${name}" doesn't exist on "${block.type}"`); + } let code = this.blockToCode(targetBlock); // Value blocks must return code and order of operations info. // Statement blocks must only return code. @@ -578,11 +574,7 @@ export class CodeGenerator { * @param _opt_thisOnly True to generate code for only this statement. * @returns Code with comments and subsequent blocks added. */ - protected scrub_( - _block: Block, - code: string, - _opt_thisOnly?: boolean, - ): string { + scrub_(_block: Block, code: string, _opt_thisOnly?: boolean): string { // Optionally override return code; } diff --git a/core/gesture.ts b/core/gesture.ts index 4b85c4f6f6b..46938d24f14 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; - } - // Then check if it was a block drag. - if (this.updateIsDraggingBlock()) { - 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'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.getFirstNonShadowBlock()); 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 28e460fa02b..1a5de250e5c 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'; @@ -66,12 +67,26 @@ export class Grid { this.update(this.scale); } + /** + * Get the spacing of the grid points (in px). + * + * @returns The spacing of the grid points. + */ + getSpacing(): number { + return this.spacing; + } + /** Sets the length of the grid lines. */ setLength(length: number) { this.length = length; this.update(this.scale); } + /** Get the length of the grid lines (in px). */ + getLength(): number { + return this.length; + } + /** * Sets whether blocks should snap to the grid or not. * @@ -85,25 +100,14 @@ export class Grid { } /** - * Whether blocks should snap to the grid, based on the initial configuration. + * Whether blocks should snap to the grid. * * @returns True if blocks should snap, false otherwise. - * @internal */ shouldSnap(): boolean { return this.snapToGrid; } - /** - * Get the spacing of the grid points (in px). - * - * @returns The spacing of the grid points. - * @internal - */ - getSpacing(): number { - return this.spacing; - } - /** * Get the ID of the pattern element, which should be randomized to avoid * conflicts with other Blockly instances on the page. @@ -181,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 7756088b6cd..df54560c557 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 b1104b157b6..6ad9532366e 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 25773c5bb1a..c5edb0f7487 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 56365ed1f98..7fb3fcf3b81 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 f0862c3e9ca..08f511a60b7 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 b938abaa4e0..323191817ac 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 767c07965f4..da7cccad588 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(); + } + } + + /** + * Initializes the fields on this input for a headless block. + * + * @internal + */ + public initModel() { + for (const field of this.fieldRow) { + field.initModel(); } - for (let i = 0; i < this.fieldRow.length; i++) { - this.fieldRow[i].init(); + } + + /** 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 96c9327fcdd..376297f10e7 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(); - - finishQueuedRenders().then(() => { - blockAnimations.connectionUiEffect(inferiorConnection.getSourceBlock()); - // bringToFront is incredibly expensive. Delay until the next frame. - setTimeout(() => { - rootBlock.bringToFront(); - }, 0); - }); - } + 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); + }); } /** @@ -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/connection_previewers/insertion_marker_previewer.ts b/core/insertion_marker_previewer.ts similarity index 69% rename from core/connection_previewers/insertion_marker_previewer.ts rename to core/insertion_marker_previewer.ts index 414d8dfe7da..3cc9f1a049f 100644 --- a/core/connection_previewers/insertion_marker_previewer.ts +++ b/core/insertion_marker_previewer.ts @@ -4,25 +4,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -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 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'; -import {ConnectionType} from '../connection_type.js'; - -/** - * An error message to throw if the block created by createMarkerBlock_ is - * missing any components. - */ -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.'; +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 renderManagement from './render_management.js'; +import * as registry from './registry.js'; +import {Renderer as ZelosRenderer} from './renderers/zelos/renderer.js'; +import {ConnectionType} from './connection_type.js'; export class InsertionMarkerPreviewer implements IConnectionPreviewer { private readonly workspace: WorkspaceSvg; @@ -122,13 +113,11 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer { private previewMarker( draggedConn: RenderedConnection, staticConn: RenderedConnection, - ): RenderedConnection { + ): RenderedConnection | null { const dragged = draggedConn.getSourceBlock(); const marker = this.createInsertionMarker(dragged); const markerConn = this.getMatchingConnection(dragged, marker, draggedConn); - if (!markerConn) { - throw Error('Could not create insertion marker to preview connection'); - } + if (!markerConn) return null; // Render disconnected from everything else so that we have a valid // connection location. @@ -144,60 +133,45 @@ 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'); + if (marker.isDeadOrDying()) return; + 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; @@ -216,7 +190,7 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer { orig: BlockSvg, marker: BlockSvg, origConn: RenderedConnection, - ) { + ): RenderedConnection | null { const origConns = orig.getConnections_(true); const markerConns = marker.getConnections_(true); if (origConns.length !== markerConns.length) return null; diff --git a/core/interfaces/i_block_dragger.ts b/core/interfaces/i_block_dragger.ts deleted file mode 100644 index 5c6a9d11239..00000000000 --- 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 00000000000..09b071110dd --- /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 89cc086169a..0467709409a 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 82d3907c5d5..86d2673bbf8 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 21ada10558b..cb723e7b88b 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 00000000000..1e8ad0ab6c4 --- /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_flyout.ts b/core/interfaces/i_flyout.ts index e73845dc73d..84067f755d2 100644 --- a/core/interfaces/i_flyout.ts +++ b/core/interfaces/i_flyout.ts @@ -12,6 +12,7 @@ import type {Coordinate} from '../utils/coordinate.js'; import type {FlyoutDefinition} from '../utils/toolbox.js'; import type {Svg} from '../utils/svg.js'; import type {IRegistrable} from './i_registrable.js'; +import {FlyoutItem} from '../flyout_base.js'; /** * Interface for a flyout. @@ -117,6 +118,16 @@ export interface IFlyout extends IRegistrable { */ show(flyoutDef: FlyoutDefinition | string): void; + /** + * Returns the list of flyout items currently present in the flyout. + * The `show` method parses the flyout definition into a list of actual + * flyout items. This method should return those concrete items, which + * may be used for e.g. keyboard navigation. + * + * @returns List of flyout items. + */ + getContents(): FlyoutItem[]; + /** * Create a copy of this block on the workspace. * diff --git a/core/interfaces/i_has_bubble.ts b/core/interfaces/i_has_bubble.ts index a2ba6093a10..276feff21e2 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 1b023970322..6b351b6b3a7 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 cb5fda09f20..61026adaeca 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 cda47c7cd9a..7cf9ad98c6f 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 2dd72c82fb7..27c945dc08b 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/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts index 97ec9f7311e..7985ac6dc24 100644 --- a/core/keyboard_nav/ast_node.ts +++ b/core/keyboard_nav/ast_node.ts @@ -12,7 +12,7 @@ */ // Former goog.module ID: Blockly.ASTNode -import type {Block} from '../block.js'; +import {Block} from '../block.js'; import type {Connection} from '../connection.js'; import {ConnectionType} from '../connection_type.js'; import type {Field} from '../field.js'; @@ -21,6 +21,9 @@ import type {IASTNodeLocation} from '../interfaces/i_ast_node_location.js'; import type {IASTNodeLocationWithBlock} from '../interfaces/i_ast_node_location_with_block.js'; import {Coordinate} from '../utils/coordinate.js'; import type {Workspace} from '../workspace.js'; +import {FlyoutButton} from '../flyout_button.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; +import {FlyoutItem} from '../flyout_base.js'; /** * Class for an AST node. @@ -286,6 +289,9 @@ export class ASTNode { if (!curLocationAsBlock || curLocationAsBlock.isDeadOrDying()) { return null; } + if (curLocationAsBlock.workspace.isFlyout) { + return this.navigateFlyoutContents(forward); + } const curRoot = curLocationAsBlock.getRootBlock(); const topBlocks = curRoot.workspace.getTopBlocks(true); for (let i = 0; i < topBlocks.length; i++) { @@ -304,6 +310,88 @@ export class ASTNode { ); } + /** + * Navigate between buttons and stacks of blocks on the flyout workspace. + * + * @param forward True to go forward. False to go backwards. + * @returns The next button, or next stack's first block, or null + */ + private navigateFlyoutContents(forward: boolean): ASTNode | null { + const nodeType = this.getType(); + let location; + let targetWorkspace; + + switch (nodeType) { + case ASTNode.types.STACK: { + location = this.getLocation() as Block; + const workspace = location.workspace as WorkspaceSvg; + targetWorkspace = workspace.targetWorkspace as WorkspaceSvg; + break; + } + case ASTNode.types.BUTTON: { + location = this.getLocation() as FlyoutButton; + targetWorkspace = location.getTargetWorkspace() as WorkspaceSvg; + break; + } + default: + return null; + } + + const flyout = targetWorkspace.getFlyout(); + if (!flyout) return null; + + const nextItem = this.findNextLocationInFlyout( + flyout.getContents(), + location, + forward, + ); + if (!nextItem) return null; + + if (nextItem.type === 'button' && nextItem.button) { + return ASTNode.createButtonNode(nextItem.button); + } else if (nextItem.type === 'block' && nextItem.block) { + return ASTNode.createStackNode(nextItem.block); + } + + return null; + } + + /** + * Finds the next (or previous if navigating backward) item in the flyout that should be navigated to. + * + * @param flyoutContents Contents of the current flyout. + * @param currentLocation Current ASTNode location. + * @param forward True if we're navigating forward, else false. + * @returns The next (or previous) FlyoutItem, or null if there is none. + */ + private findNextLocationInFlyout( + flyoutContents: FlyoutItem[], + currentLocation: IASTNodeLocation, + forward: boolean, + ): FlyoutItem | null { + const currentIndex = flyoutContents.findIndex((item: FlyoutItem) => { + if (currentLocation instanceof Block && item.block === currentLocation) { + return true; + } + if ( + currentLocation instanceof FlyoutButton && + item.button === currentLocation + ) { + return true; + } + return false; + }); + + if (currentIndex < 0) return null; + + const resultIndex = forward ? currentIndex + 1 : currentIndex - 1; + if (resultIndex === -1 || resultIndex === flyoutContents.length) { + return null; + } + + return flyoutContents[resultIndex]; + } + /** * Finds the top most AST node for a given block. * This is either the previous connection, output connection or block @@ -385,7 +473,7 @@ export class ASTNode { * Finds the source block of the location of this node. * * @returns The source block of the location, or null if the node is of type - * workspace. + * workspace or button. */ getSourceBlock(): Block | null { if (this.getType() === ASTNode.types.BLOCK) { @@ -394,6 +482,8 @@ export class ASTNode { return this.getLocation() as Block; } else if (this.getType() === ASTNode.types.WORKSPACE) { return null; + } else if (this.getType() === ASTNode.types.BUTTON) { + return null; } else { return (this.getLocation() as IASTNodeLocationWithBlock).getSourceBlock(); } @@ -435,6 +525,8 @@ export class ASTNode { const targetConnection = connection.targetConnection; return ASTNode.createConnectionNode(targetConnection!); } + case ASTNode.types.BUTTON: + return this.navigateFlyoutContents(true); } return null; @@ -513,6 +605,8 @@ export class ASTNode { const connection = this.location as Connection; return ASTNode.createBlockNode(connection.getSourceBlock()); } + case ASTNode.types.BUTTON: + return this.navigateFlyoutContents(false); } return null; @@ -688,6 +782,22 @@ export class ASTNode { return new ASTNode(ASTNode.types.STACK, topBlock); } + /** + * Create an AST node of type button. A button in this case refers + * specifically to a button in a flyout. + * + * @param button A top block has no parent and can be found in the list + * returned by workspace.getTopBlocks(). + * @returns An AST node of type stack that points to the top block on the + * stack. + */ + static createButtonNode(button: FlyoutButton): ASTNode | null { + if (!button) { + return null; + } + return new ASTNode(ASTNode.types.BUTTON, button); + } + /** * Creates an AST node pointing to a workspace. * @@ -740,6 +850,7 @@ export namespace ASTNode { PREVIOUS = 'previous', STACK = 'stack', WORKSPACE = 'workspace', + BUTTON = 'button', } } diff --git a/core/layer_manager.ts b/core/layer_manager.ts index c27339d9b7d..e181e680139 100644 --- a/core/layer_manager.ts +++ b/core/layer_manager.ts @@ -15,6 +15,8 @@ import {Coordinate} from './utils/coordinate.js'; export class LayerManager { /** The layer elements being dragged are appended to. */ private dragLayer: SVGGElement | undefined; + /** The layer elements being animated are appended to. */ + private animationLayer: SVGGElement | undefined; /** The layers elements not being dragged are appended to. */ private layers = new Map(); @@ -26,6 +28,7 @@ export class LayerManager { // been appended yet. if (injectionDiv) { this.dragLayer = this.createDragLayer(injectionDiv); + this.animationLayer = this.createAnimationLayer(injectionDiv); } // We construct these manually so we can add the css class for backwards @@ -48,6 +51,35 @@ export class LayerManager { return dom.createSvgElement(Svg.G, {}, svg); } + private createAnimationLayer(injectionDiv: Element) { + const svg = dom.createSvgElement(Svg.SVG, { + 'class': 'blocklyAnimationLayer', + 'xmlns': dom.SVG_NS, + 'xmlns:html': dom.HTML_NS, + 'xmlns:xlink': dom.XLINK_NS, + 'version': '1.1', + }); + injectionDiv.append(svg); + return dom.createSvgElement(Svg.G, {}, svg); + } + + /** + * Appends the element to the animation layer. The animation layer doesn't + * move when the workspace moves, so e.g. delete animations don't move + * when a block delete triggers a workspace resize. + * + * @internal + */ + appendToAnimationLayer(elem: IRenderedElement) { + const currentTransform = this.dragLayer?.getAttribute('transform'); + // Only update the current transform when appending, so animations don't + // move if the workspace moves. + if (currentTransform) { + this.animationLayer?.setAttribute('transform', currentTransform); + } + this.animationLayer?.appendChild(elem.getSvgRoot()); + } + /** * Translates layers when the workspace is dragged or zoomed. * diff --git a/core/main.ts b/core/main.ts index f794a8ab310..c301e989034 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/names.ts b/core/names.ts index ce8c626db85..4f4c72faac8 100644 --- a/core/names.ts +++ b/core/names.ts @@ -218,7 +218,7 @@ export class Names { // https://github.com/google/blockly/issues/1654 name = encodeURI(name.replace(/ /g, '_')).replace(/[^\w]/g, '_'); // Most languages don't allow names with leading numbers. - if ('0123456789'.indexOf(name[0]) !== -1) { + if ('0123456789'.includes(name[0])) { name = 'my_' + name; } } diff --git a/core/options.ts b/core/options.ts index 3b6483a2642..42d2b41de73 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 3645a6fdf80..d46c36f4819 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'); @@ -162,8 +166,13 @@ export function register( // Validate that the given class has all the required properties. validate(type, registryItem); - // Don't throw an error if opt_allowOverrides is true. - if (!opt_allowOverrides && typeRegistry[caselessName]) { + // Don't throw an error if opt_allowOverrides is true, + // or if we're trying to register the same item. + if ( + !opt_allowOverrides && + typeRegistry[caselessName] && + typeRegistry[caselessName] !== registryItem + ) { throw Error( 'Name "' + caselessName + diff --git a/core/render_management.ts b/core/render_management.ts index f0ec2a13314..3e84efead24 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); } @@ -155,7 +170,7 @@ function dequeueBlock(block: BlockSvg) { * No need to render dead blocks. * * No need to render blocks with parents. A render for the block may have been - * queued, and the the block was connected to a parent, so it is no longer a + * queued, and the block was connected to a parent, so it is no longer a * root block. Rendering will be triggered through the real root block. */ function shouldRenderRootBlock(block: BlockSvg): boolean { @@ -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 8953ec97776..5aa6f7f7588 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/constants.ts b/core/renderers/common/constants.ts index 1983793f051..078fc01d648 100644 --- a/core/renderers/common/constants.ts +++ b/core/renderers/common/constants.ts @@ -617,7 +617,7 @@ export class ConstantProvider { getBlockStyle(blockStyleName: string | null): BlockStyle { return ( this.blockStyles[blockStyleName || ''] || - (blockStyleName && blockStyleName.indexOf('auto_') === 0 + (blockStyleName && blockStyleName.startsWith('auto_') ? this.getBlockStyleForColour(blockStyleName.substring(5)).style : this.createBlockStyle_('#000000')) ); diff --git a/core/renderers/common/drawer.ts b/core/renderers/common/drawer.ts index 83732516c57..5ded620b799 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/common/marker_svg.ts b/core/renderers/common/marker_svg.ts index 5581dc3173c..995783576ef 100644 --- a/core/renderers/common/marker_svg.ts +++ b/core/renderers/common/marker_svg.ts @@ -24,6 +24,7 @@ import * as svgPaths from '../../utils/svg_paths.js'; import type {WorkspaceSvg} from '../../workspace_svg.js'; import type {ConstantProvider, Notch, PuzzleTab} from './constants.js'; +import {FlyoutButton} from '../../flyout_button.js'; /** The name of the CSS class for a cursor. */ const CURSOR_CLASS = 'blocklyCursor'; @@ -205,6 +206,8 @@ export class MarkerSvg { this.showWithCoordinates_(curNode); } else if (curNode.getType() === ASTNode.types.STACK) { this.showWithStack_(curNode); + } else if (curNode.getType() === ASTNode.types.BUTTON) { + this.showWithButton_(curNode); } } @@ -378,6 +381,38 @@ export class MarkerSvg { this.showCurrent_(); } + /** + * Position and display the marker for a flyout button. + * This is a box with extra padding around the button. + * + * @param curNode The node to draw the marker for. + */ + protected showWithButton_(curNode: ASTNode) { + const button = curNode.getLocation() as FlyoutButton; + + // Gets the height and width of entire stack. + const heightWidth = {height: button.height, width: button.width}; + + // Add padding so that being on a button looks similar to being on a stack. + const width = heightWidth.width + this.constants_.CURSOR_STACK_PADDING; + const height = heightWidth.height + this.constants_.CURSOR_STACK_PADDING; + + // Shift the rectangle slightly to upper left so padding is equal on all + // sides. + const xPadding = -this.constants_.CURSOR_STACK_PADDING / 2; + const yPadding = -this.constants_.CURSOR_STACK_PADDING / 2; + + let x = xPadding; + const y = yPadding; + + if (this.workspace.RTL) { + x = -(width + xPadding); + } + this.positionRect_(x, y, width, height); + this.setParent_(button); + this.showCurrent_(); + } + /** Show the current marker. */ protected showCurrent_() { this.hide(); diff --git a/core/renderers/measurables/icon.ts b/core/renderers/measurables/icon.ts index d681dbbd9a4..98e3f722d00 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 83bfaff0a57..00000000000 --- 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 d0985674c39..00000000000 --- 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 ae86e3eb692..00000000000 --- 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 4ec0a220c52..00000000000 --- 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 b15b48f197c..00000000000 --- 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/renderers/zelos/constants.ts b/core/renderers/zelos/constants.ts index 1eabd7edd05..c50e66510a0 100644 --- a/core/renderers/zelos/constants.ts +++ b/core/renderers/zelos/constants.ts @@ -536,13 +536,13 @@ export class ConstantProvider extends BaseConstantProvider { } } // Includes doesn't work in IE. - if (checks && checks.indexOf('Boolean') !== -1) { + if (checks && checks.includes('Boolean')) { return this.HEXAGONAL!; } - if (checks && checks.indexOf('Number') !== -1) { + if (checks && checks.includes('Number')) { return this.ROUNDED!; } - if (checks && checks.indexOf('String') !== -1) { + if (checks && checks.includes('String')) { return this.ROUNDED!; } return this.ROUNDED!; @@ -794,6 +794,10 @@ export class ConstantProvider extends BaseConstantProvider { `pt ${this.FIELD_TEXT_FONTFAMILY};`, `}`, + `${selector} .blocklyTextInputBubble textarea {`, + `font-weight: normal;`, + `}`, + // Fields. `${selector} .blocklyText {`, `fill: #fff;`, diff --git a/core/serialization.ts b/core/serialization.ts index 8362f2ced7a..8e159bb2b0b 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 229686a0de6..dbb58cffb2a 100644 --- a/core/serialization/blocks.ts +++ b/core/serialization/blocks.ts @@ -9,6 +9,7 @@ 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 eventUtils from '../events/utils.js'; import {inputTypes} from '../inputs/input_types.js'; import {isSerializable} from '../interfaces/i_serializable.js'; @@ -28,6 +29,8 @@ import { } from './exceptions.js'; import * as priorities from './priorities.js'; import * as serializationRegistry from './registry.js'; +import * as Variables from '../variables.js'; +import {VariableModel} from '../variable_model.js'; // TODO(#5160): Remove this once lint is fixed. /* eslint-disable no-use-before-define */ @@ -53,6 +56,7 @@ export interface State { movable?: boolean; editable?: boolean; enabled?: boolean; + disabledReasons?: string[]; inline?: boolean; data?: string; extraState?: AnyDuringMigration; @@ -158,7 +162,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; @@ -415,6 +419,7 @@ export function appendInternal( } eventUtils.disable(); + const variablesBeforeCreation = workspace.getAllVariables(); let block; try { block = appendPrivate(state, workspace, {parentConnection, isShadow}); @@ -422,7 +427,12 @@ export function appendInternal( eventUtils.enable(); } + // Fire a VarCreate event for each (if any) new variable created. + checkNewVariables(workspace, variablesBeforeCreation); + if (eventUtils.isEnabled()) { + // Block events come after var events, in case they refer to newly created + // variables. eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block)); } eventUtils.setGroup(existingGroup); @@ -483,6 +493,33 @@ function appendPrivate( return block; } +/** + * Checks the workspace for any new variables that were created during the + * deserialization of a block and fires a VarCreate event for each. + * + * @param workspace The workspace where new variables are being created + * @param originalVariables The array of variables that existed in the workspace + * before adding the new block. + */ +function checkNewVariables( + workspace: Workspace, + originalVariables: VariableModel[], +) { + if (eventUtils.isEnabled()) { + const newVariables = Variables.getAddedVariables( + workspace, + originalVariables, + ); + // Fire a VarCreate event for each (if any) new variable created. + for (let i = 0; i < newVariables.length; i++) { + const thisVariable = newVariables[i]; + eventUtils.fire( + new (eventUtils.get(eventUtils.VAR_CREATE))(thisVariable), + ); + } + } +} + /** * Applies any coordinate information available on the state object to the * block. @@ -520,7 +557,14 @@ function loadAttributes(block: Block, state: State) { block.setEditable(false); } if (state['enabled'] === false) { - block.setEnabled(false); + // Before May 2024 we just used 'enabled', with no reasons. + // Contiune to support this syntax. + 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 c0e78116655..726242f0157 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 34d381171a1..55d72060417 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; + + /** + * 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 IProcedureModel to JSON. - * - * @internal */ export function saveProcedure(proc: IProcedureModel): State { - const state: State = { - id: proc.getId(), - name: proc.getName(), - returnTypes: proc.getReturnTypes(), - }; + const state: State = proc.saveState(); if (!proc.getParameters().length) return state; - state.parameters = proc.getParameters().map((param) => saveParameter(param)); - return state; -} - -/** - * Serializes the given IParameterModel to JSON. - * - * @internal - */ -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(); + 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 00000000000..525274e58e4 --- /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/serialization/workspaces.ts b/core/serialization/workspaces.ts index 07c909ccf5c..871060196c7 100644 --- a/core/serialization/workspaces.ts +++ b/core/serialization/workspaces.ts @@ -75,8 +75,7 @@ export function load( } // reverse() is destructive, so we have to re-reverse to correct the order. - for (let [name, deserializer] of deserializers.reverse()) { - name = name; + for (const [name, deserializer] of deserializers.reverse()) { const pluginState = state[name]; if (pluginState) { (deserializer as ISerializer)?.load(state[name], workspace); diff --git a/core/shortcut_items.ts b/core/shortcut_items.ts index 57b90a498a7..b5abf5554ee 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/shortcut_registry.ts b/core/shortcut_registry.ts index 556efb148a3..161a2ed1495 100644 --- a/core/shortcut_registry.ts +++ b/core/shortcut_registry.ts @@ -287,7 +287,7 @@ export class ShortcutRegistry { } } if (serializedKey !== '' && e.keyCode) { - serializedKey = serializedKey + '+' + e.keyCode; + serializedKey += '+' + e.keyCode; } else if (e.keyCode) { serializedKey = String(e.keyCode); } @@ -325,7 +325,7 @@ export class ShortcutRegistry { const modifierKeyCode = ( ShortcutRegistry.modifierKeys as AnyDuringMigration )[modifier]; - if (modifiers.indexOf(modifierKeyCode) > -1) { + if (modifiers.includes(modifierKeyCode)) { if (serializedKey !== '') { serializedKey += '+'; } @@ -335,7 +335,7 @@ export class ShortcutRegistry { } if (serializedKey !== '' && keyCode) { - serializedKey = serializedKey + '+' + keyCode; + serializedKey += '+' + keyCode; } else if (keyCode) { serializedKey = `${keyCode}`; } diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 19a88184c73..e0fb62e23da 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 42c32ea1f34..050f506a48f 100644 --- a/core/trashcan.ts +++ b/core/trashcan.ts @@ -616,7 +616,7 @@ export class Trashcan const cleanedJson = JSON.stringify( this.cleanBlockJson(deleteEvent.oldJson), ); - if (this.contents.indexOf(cleanedJson) !== -1) { + if (this.contents.includes(cleanedJson)) { return; } this.contents.unshift(cleanedJson); @@ -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 e9efca9f8e9..e318e7e915e 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 ab88d2c8446..705dc2c2897 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 046415bdadc..b7842c948ca 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. * @@ -225,9 +204,9 @@ function wrapScore( score -= Math.pow(maxLength - lineLengths[i], 1.5); // Optimize for structure. // Add score to line endings after punctuation. - if ('.?!'.indexOf(linePunctuation[i]) !== -1) { + if ('.?!'.includes(linePunctuation[i])) { score += limit / 3; - } else if (',;)]}'.indexOf(linePunctuation[i]) !== -1) { + } else if (',;)]}'.includes(linePunctuation[i])) { score += limit / 4; } } diff --git a/core/utils/svg_math.ts b/core/utils/svg_math.ts index bb095932f30..0444787add7 100644 --- a/core/utils/svg_math.ts +++ b/core/utils/svg_math.ts @@ -62,7 +62,7 @@ export function getRelativeXY(element: Element): Coordinate { // Then check for style = transform: translate(...) or translate3d(...) const style = element.getAttribute('style'); - if (style && style.indexOf('translate') > -1) { + if (style && style.includes('translate')) { const styleComponents = style.match(XY_STYLE_REGEX); if (styleComponents) { xy.x += Number(styleComponents[1]); @@ -87,10 +87,10 @@ export function getInjectionDivXY(element: Element): Coordinate { let y = 0; while (element) { const xy = getRelativeXY(element); - x = x + xy.x; - y = y + xy.y; + x += xy.x; + y += xy.y; const classes = element.getAttribute('class') || ''; - if ((' ' + classes + ' ').indexOf(' injectionDiv ') !== -1) { + if ((' ' + classes + ' ').includes(' injectionDiv ')) { break; } element = element.parentNode as Element; diff --git a/core/utils/toolbox.ts b/core/utils/toolbox.ts index 699150f1c1f..fbf031ac612 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; @@ -381,7 +382,7 @@ function addAttributes(node: Node, obj: AnyDuringMigration) { // AnyDuringMigration because: Property 'attributes' does not exist on type // 'Node'. const attr = (node as AnyDuringMigration).attributes[j]; - if (attr.nodeName.indexOf('css-') > -1) { + if (attr.nodeName.includes('css-')) { obj['cssconfig'] = obj['cssconfig'] || {}; obj['cssconfig'][attr.nodeName.replace('css-', '')] = attr.value; } else { diff --git a/core/utils/useragent.ts b/core/utils/useragent.ts index 078d79a66dd..92180c21db0 100644 --- a/core/utils/useragent.ts +++ b/core/utils/useragent.ts @@ -37,7 +37,7 @@ let isMobile: boolean; * @returns True if name is present. */ function has(name: string): boolean { - return rawUpper.indexOf(name.toUpperCase()) !== -1; + return rawUpper.includes(name.toUpperCase()); } // Browsers. Logic from: diff --git a/core/variables.ts b/core/variables.ts index 74c2b925241..dee53a72bc8 100644 --- a/core/variables.ts +++ b/core/variables.ts @@ -642,7 +642,7 @@ export function getAddedVariables( const variable = allCurrentVariables[i]; // For any variable that is present in allCurrentVariables but not // present in originalVariables, add the variable to addedVariables. - if (originalVariables.indexOf(variable) === -1) { + if (!originalVariables.includes(variable)) { addedVariables.push(variable); } } diff --git a/core/widgetdiv.ts b/core/widgetdiv.ts index 52816f36301..e03cf0b2588 100644 --- a/core/widgetdiv.ts +++ b/core/widgetdiv.ts @@ -57,14 +57,16 @@ export function testOnly_setDiv(newDiv: HTMLDivElement | null) { * Create the widget div and inject it onto the page. */ export function createDom() { + const container = common.getParentContainer() || document.body; + if (document.querySelector('.' + containerClassName)) { - return; // Already created. + containerDiv = document.querySelector('.' + containerClassName); + } else { + containerDiv = document.createElement('div') as HTMLDivElement; + containerDiv.className = containerClassName; } - containerDiv = document.createElement('div') as HTMLDivElement; - containerDiv.className = containerClassName; - const container = common.getParentContainer() || document.body; - container.appendChild(containerDiv); + container.appendChild(containerDiv!); } /** diff --git a/core/workspace.ts b/core/workspace.ts index dbbcb7743f1..2e9b2b0a1e9 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 4149bcff48f..00000000000 --- 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 0aa4292c684..00000000000 --- 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 0eb7e808756..14cc1101f47 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; @@ -695,7 +690,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { let element: Element = this.svgGroup_; while (element) { const classes = element.getAttribute('class') || ''; - if ((' ' + classes + ' ').indexOf(' injectionDiv ') !== -1) { + if ((' ' + classes + ' ').includes(' injectionDiv ')) { this.injectionDiv = element; break; } @@ -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() @@ -1306,190 +1297,13 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { // Using Set here would be great, but at the cost of IE10 support. if (!state) { arrayUtils.removeElem(this.highlightedBlocks, block); - } else if (this.highlightedBlocks.indexOf(block) === -1) { + } else if (!this.highlightedBlocks.includes(block)) { this.highlightedBlocks.push(block); } block.setHighlighted(state); } } - /** - * 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 ee72f526bd9..bad381c5d85 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -9,6 +9,7 @@ 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 eventUtils from './events/utils.js'; import type {Field} from './field.js'; import {IconType} from './icons/icon_types.js'; @@ -19,22 +20,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 +42,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 +273,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 +474,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 +501,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. @@ -899,7 +953,7 @@ function applyNextTagNodes( * * @param xmlBlock XML block element. * @param workspace The workspace. - * @param parentConnection The parent connection to to connect this block to + * @param parentConnection The parent connection to connect this block to * after instantiating. * @param connectedToParentNext Whether the provided parent connection is a next * connection, rather than output or statement. @@ -968,7 +1022,20 @@ function domToBlockHeadless( } const disabled = xmlBlock.getAttribute('disabled'); if (disabled) { - block.setEnabled(disabled !== 'true' && disabled !== 'disabled'); + // Before May 2024 we just used 'disabled', with no reasons. + // Contiune to support this syntax. + 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/app_controller.js b/demos/blockfactory/app_controller.js index 5698033fe12..fcfa2296dcf 100644 --- a/demos/blockfactory/app_controller.js +++ b/demos/blockfactory/app_controller.js @@ -126,6 +126,16 @@ AppController.prototype.exportBlockLibraryToFile = function() { } }; +AppController.prototype.exportBlockLibraryAsJson = function() { + const blockJson = this.blockLibraryController.getBlockLibraryAsJson(); + if (blockJson.length === 0) { + alert('No blocks in library to export'); + return; + } + const filename = 'legacy_block_factory_export.txt'; + FactoryUtils.createAndDownloadFile(JSON.stringify(blockJson), filename, 'plain'); +}; + /** * Converts an object mapping block type to XML to text file for output. * @param {!Object} blockXmlMap Object mapping block type to XML. @@ -491,6 +501,10 @@ AppController.prototype.assignBlockFactoryClickHandlers = function() { self.exportBlockLibraryToFile(); }); + document.getElementById('exportAsJson').addEventListener('click', function() { + self.exportBlockLibraryAsJson(); + }); + document.getElementById('helpButton').addEventListener('click', function() { open('https://developers.google.com/blockly/custom-blocks/block-factory', diff --git a/demos/blockfactory/block_definition_extractor.js b/demos/blockfactory/block_definition_extractor.js index e6b1cedd619..fa1aae7750e 100644 --- a/demos/blockfactory/block_definition_extractor.js +++ b/demos/blockfactory/block_definition_extractor.js @@ -71,7 +71,7 @@ BlockDefinitionExtractor.newDomElement_ = function(name, opt_attrs, opt_text) { * requested type. * * @param {string} type Type name of desired connection constraint. - * @return {!Element} The representing the the constraint type. + * @return {!Element} The representing the constraint type. * @private */ BlockDefinitionExtractor.buildBlockForType_ = function(type) { diff --git a/demos/blockfactory/block_exporter_controller.js b/demos/blockfactory/block_exporter_controller.js index bd41f2e74d4..bccd8087de0 100644 --- a/demos/blockfactory/block_exporter_controller.js +++ b/demos/blockfactory/block_exporter_controller.js @@ -224,9 +224,9 @@ BlockExporterController.prototype.selectUsedBlocks = function() { var unstoredCustomBlockTypes = []; for (var i = 0, blockType; blockType = this.usedBlockTypes[i]; i++) { - if (storedBlockTypes.indexOf(blockType) !== -1) { + if (storedBlockTypes.includes(blockType)) { sharedBlockTypes.push(blockType); - } else if (StandardCategories.coreBlockTypes.indexOf(blockType) === -1) { + } else if (!StandardCategories.coreBlockTypes.includes(blockType)) { unstoredCustomBlockTypes.push(blockType); } } diff --git a/demos/blockfactory/block_library_controller.js b/demos/blockfactory/block_library_controller.js index 2192a7bdd11..7bb34e8d623 100644 --- a/demos/blockfactory/block_library_controller.js +++ b/demos/blockfactory/block_library_controller.js @@ -173,6 +173,29 @@ BlockLibraryController.prototype.getBlockLibrary = function() { return this.storage.getBlockXmlTextMap(); }; +/** + * @return {Object[]} Array of JSON data, where each item is the data for one block type. + */ +BlockLibraryController.prototype.getBlockLibraryAsJson = function() { + const xmlBlocks = this.storage.getBlockXmlMap(this.storage.getBlockTypes()); + const jsonBlocks = []; + const headlessWorkspace = new Blockly.Workspace(); + + for (const blockName in xmlBlocks) { + // Load the block XML into a workspace so we can save it as JSON + headlessWorkspace.clear(); + const blockXml = xmlBlocks[blockName]; + Blockly.Xml.domToWorkspace(blockXml, headlessWorkspace); + const block = headlessWorkspace.getBlocksByType('factory_base', false)[0]; + + if (!block) continue; + + const json = Blockly.serialization.blocks.save(block, {addCoordinates: false, saveIds: false}); + jsonBlocks.push(json); + } + return jsonBlocks; +} + /** * Return stored XML of a given block type. * @param {string} blockType The type of block. diff --git a/demos/blockfactory/blocks.js b/demos/blockfactory/blocks.js index 70691e2649e..15c8ac864d6 100644 --- a/demos/blockfactory/blocks.js +++ b/demos/blockfactory/blocks.js @@ -682,7 +682,7 @@ Blockly.Blocks['type_group'] = { // 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.indexOf(connection) === -1) { + if (connection && !connections.includes(connection)) { connection.disconnect(); } } @@ -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.js b/demos/blockfactory/factory.js index a77d8467a45..07ee889de7f 100644 --- a/demos/blockfactory/factory.js +++ b/demos/blockfactory/factory.js @@ -238,7 +238,7 @@ BlockFactory.updatePreview = function() { // Warn user only if their block type is already exists in Blockly's // standard library. var rootBlock = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); - if (StandardCategories.coreBlockTypes.indexOf(blockType) !== -1) { + if (StandardCategories.coreBlockTypes.includes(blockType)) { rootBlock.setWarningText('A core Blockly block already exists ' + 'under this name.'); diff --git a/demos/blockfactory/factory_utils.js b/demos/blockfactory/factory_utils.js index 164b3357be0..4731d1ce9e7 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' @@ -594,7 +594,7 @@ FactoryUtils.getOptTypesFrom = function(block, name) { var types = FactoryUtils.getTypesFrom_(block, name); if (types.length === 0) { return undefined; - } else if (types.indexOf('null') !== -1) { + } else if (types.includes('null')) { return 'null'; } else if (types.length === 1) { return types[0]; @@ -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'))]; @@ -771,7 +771,7 @@ FactoryUtils.parseJsBlockDefinitions = function(blockDefsString) { var blockDefArray = []; var defStart = blockDefsString.indexOf('Blockly.Blocks'); - while (blockDefsString.indexOf('Blockly.Blocks', defStart) !== -1) { + while (blockDefsString.includes('Blockly.Blocks', defStart)) { var nextStart = blockDefsString.indexOf('Blockly.Blocks', defStart + 1); if (nextStart === -1) { // This is the last block definition. @@ -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/index.html b/demos/blockfactory/index.html index 14d84d73f7e..77687bce14a 100644 --- a/demos/blockfactory/index.html +++ b/demos/blockfactory/index.html @@ -4,10 +4,10 @@ Blockly Demo: Blockly Developer Tools - - - - + + + + @@ -339,6 +339,9 @@

Preview: + @@ -369,7 +372,7 @@

Block Definition: // Manual JavaScript works but requires use of eval(). // TODO(#1269): Replace eval() with JS-Interpreter before // re-enabling "Manual JavaScript" mode. - if (document.location.href.indexOf('file://') === 0) { + if (document.location.href.startsWith('file://')) { document.write( ''); } diff --git a/demos/blockfactory/workspacefactory/wfactory_model.js b/demos/blockfactory/workspacefactory/wfactory_model.js index 9b9586ea134..b0e1ab9d4f3 100644 --- a/demos/blockfactory/workspacefactory/wfactory_model.js +++ b/demos/blockfactory/workspacefactory/wfactory_model.js @@ -395,7 +395,7 @@ WorkspaceFactoryModel.prototype.getAllUsedBlockTypes = function() { // Add block types if not already in list. for (var i = 0; i < blocks.length; i++) { var type = blocks[i].getAttribute('type'); - if (list.indexOf(type) === -1) { + if (!list.includes(type)) { list.push(type); } } @@ -444,9 +444,9 @@ WorkspaceFactoryModel.prototype.updateLibBlockTypes = function(blockTypes) { */ WorkspaceFactoryModel.prototype.isDefinedBlockType = function(blockType) { var isStandardBlock = - StandardCategories.coreBlockTypes.indexOf(blockType) !== -1; - var isLibBlock = this.libBlockTypes.indexOf(blockType) !== -1; - var isImportedBlock = this.importedBlockTypes.indexOf(blockType) !== -1; + StandardCategories.coreBlockTypes.includes(blockType); + var isLibBlock = this.libBlockTypes.includes(blockType); + var isImportedBlock = this.importedBlockTypes.includes(blockType); return (isStandardBlock || isLibBlock || isImportedBlock); }; diff --git a/demos/blockfactory_old/blocks.js b/demos/blockfactory_old/blocks.js deleted file mode 100644 index 9d6adc9e88b..00000000000 --- 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.indexOf(connection) === -1) { - 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 7ac395032d5..00000000000 --- 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.indexOf('null') !== -1) { - 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 4f8b72f41ed..00000000000
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 2509e71383f..00000000000
--- 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 11dfd82845e..00000000000 Binary files a/demos/blockfactory_old/link.png and /dev/null differ diff --git a/demos/code/code.js b/demos/code/code.js index 63b776aab9c..c264ff2dc9d 100644 --- a/demos/code/code.js +++ b/demos/code/code.js @@ -106,7 +106,7 @@ Code.getLang = function() { * @return {boolean} True if RTL, false if LTR. */ Code.isRtl = function() { - return Code.LANGUAGE_RTL.indexOf(Code.LANG) !== -1; + return Code.LANGUAGE_RTL.includes(Code.LANG); }; /** @@ -396,7 +396,7 @@ Code.checkAllGeneratorFunctionsDefined = function(generator) { for (var i = 0; i < blocks.length; i++) { var blockType = blocks[i].type; if (!generator.forBlock[blockType]) { - if (missingBlockGenerators.indexOf(blockType) === -1) { + if (!missingBlockGenerators.includes(blockType)) { missingBlockGenerators.push(blockType); } } @@ -450,7 +450,7 @@ Code.init = function() { // TODO: Clean up the message files so this is done explicitly instead of // through this for-loop. for (var messageKey in MSG) { - if (messageKey.indexOf('cat') === 0) { + if (messageKey.startsWith('cat')) { Blockly.Msg[messageKey.toUpperCase()] = MSG[messageKey]; } } diff --git a/demos/code/index.html b/demos/code/index.html index a1a8661ecdb..d8f894607f7 100644 --- a/demos/code/index.html +++ b/demos/code/index.html @@ -350,44 +350,6 @@

Blockly‏ > - - - - - - - 100 - - - - - 50 - - - - - 0 - - - - - - - #ff0000 - - - - - #3333ff - - - - - 0.5 - - - - diff --git a/generators/dart.ts b/generators/dart.ts index ee21b1fbddf..5587e94f162 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 ac72fc04c84..00000000000 --- 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/procedures.ts b/generators/dart/procedures.ts index 5102432ad22..0ecf6d21008 100644 --- a/generators/dart/procedures.ts +++ b/generators/dart/procedures.ts @@ -35,8 +35,17 @@ export function procedures_defreturn(block: Block, generator: DartGenerator) { generator.INDENT, ); } - const branch = generator.statementToCode(block, 'STACK'); - let returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + let branch = ''; + if (block.getInput('STACK')) { + // The 'procedures_defreturn' block might not have a STACK input. + branch = generator.statementToCode(block, 'STACK'); + } + let returnValue = ''; + if (block.getInput('RETURN')) { + // The 'procedures_defnoreturn' block (which shares this code) + // does not have a RETURN input. + returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + } let xfix2 = ''; if (branch && returnValue) { // After executing the function body, revisit this block for the return. diff --git a/generators/dart/text.ts b/generators/dart/text.ts index 1085160194e..c141eaad0fa 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.indexOf('+') !== -1 ? Order.ADDITIVE : Order.ATOMIC; - return [code, order]; -} - export function text_join( block: Block, generator: DartGenerator, @@ -128,10 +118,12 @@ export function text_charAt( return [code, Order.UNARY_POSTFIX]; } case 'LAST': - at = 1; - // Fall through. case 'FROM_END': { - at = generator.getAdjusted(block, 'AT', 1); + if (where === 'LAST') { + at = 1; + } else { + at = generator.getAdjusted(block, 'AT', 1); + } const functionName = generator.provideFunction_( 'text_get_from_end', ` @@ -140,7 +132,7 @@ String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String text, num x) { } `, ); - const code = functionName + '(' + text + ', ' + at + ')'; + const code = `${functionName}(${text}, ${at})`; return [code, Order.UNARY_POSTFIX]; } case 'RANDOM': { diff --git a/generators/javascript.ts b/generators/javascript.ts index 2358d187d3d..5fa6bb84d0a 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 b599e76d9e7..00000000000 --- 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/javascript_generator.ts b/generators/javascript/javascript_generator.ts index 02602e71fa4..592563f919d 100644 --- a/generators/javascript/javascript_generator.ts +++ b/generators/javascript/javascript_generator.ts @@ -251,7 +251,6 @@ export class JavascriptGenerator extends CodeGenerator { * @param code The JavaScript code created for this block. * @param thisOnly True to generate code for only this statement. * @returns JavaScript code with comments and subsequent blocks added. - * @protected */ scrub_(block: Block, code: string, thisOnly = false): string { let commentCode = ''; diff --git a/generators/javascript/math.ts b/generators/javascript/math.ts index e8ab2852f38..238ad19a2a1 100644 --- a/generators/javascript/math.ts +++ b/generators/javascript/math.ts @@ -318,7 +318,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(values) { } for (var j = 0; j < counts.length; j++) { if (counts[j][1] === maxCount) { - modes.push(counts[j][0]); + modes.push(counts[j][0]); } } return modes; @@ -341,7 +341,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(numbers) { for (var j = 0; j < n; j++) { variance += Math.pow(numbers[j] - mean, 2); } - variance = variance / n; + variance /= n; return Math.sqrt(variance); } `, diff --git a/generators/javascript/procedures.ts b/generators/javascript/procedures.ts index a835271e7d7..e0a055217cf 100644 --- a/generators/javascript/procedures.ts +++ b/generators/javascript/procedures.ts @@ -38,8 +38,17 @@ export function procedures_defreturn( generator.INDENT, ); } - const branch = generator.statementToCode(block, 'STACK'); - let returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + let branch = ''; + if (block.getInput('STACK')) { + // The 'procedures_defreturn' block might not have a STACK input. + branch = generator.statementToCode(block, 'STACK'); + } + let returnValue = ''; + if (block.getInput('RETURN')) { + // The 'procedures_defnoreturn' block (which shares this code) + // does not have a RETURN input. + returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + } let xfix2 = ''; if (branch && returnValue) { // After executing the function body, revisit this block for the return. diff --git a/generators/javascript/text.ts b/generators/javascript/text.ts index 1681979ec92..d31bdcf4b95 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.indexOf('+') !== -1 ? 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 5c5b0af008e..0cf81fb961f 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 f4a6a8ea988..00000000000 --- 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 1af18b95582..901ed88a671 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/loops.ts b/generators/lua/loops.ts index fec175b4841..01474cd81c5 100644 --- a/generators/lua/loops.ts +++ b/generators/lua/loops.ts @@ -35,7 +35,7 @@ const CONTINUE_STATEMENT = 'goto continue\n'; * @returns Generated label or '' if unnecessary */ function addContinueLabel(branch: string, indent: string): string { - if (branch.indexOf(CONTINUE_STATEMENT) !== -1) { + if (branch.includes(CONTINUE_STATEMENT)) { // False positives are possible (e.g. a string literal), but are harmless. return branch + indent + '::continue::\n'; } else { diff --git a/generators/lua/procedures.ts b/generators/lua/procedures.ts index 07003aee9be..79dc58aa252 100644 --- a/generators/lua/procedures.ts +++ b/generators/lua/procedures.ts @@ -38,8 +38,17 @@ export function procedures_defreturn( generator.INDENT, ); } - let branch = generator.statementToCode(block, 'STACK'); - let returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + let branch = ''; + if (block.getInput('STACK')) { + // The 'procedures_defreturn' block might not have a STACK input. + branch = generator.statementToCode(block, 'STACK'); + } + let returnValue = ''; + if (block.getInput('RETURN')) { + // The 'procedures_defnoreturn' block (which shares this code) + // does not have a RETURN input. + returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + } let xfix2 = ''; if (branch && returnValue) { // After executing the function body, revisit this block for the return. diff --git a/generators/lua/text.ts b/generators/lua/text.ts index ff65327cf9d..1c4a79a8e96 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.indexOf('..') !== -1 ? Order.CONCATENATION : Order.ATOMIC; - return [code, order]; -} - export function text_join( block: Block, generator: LuaGenerator, @@ -132,8 +122,6 @@ export function text_charAt( // Get letter at index. // Note: Until January 2013 this block did not have the WHERE input. const where = block.getFieldValue('WHERE') || 'FROM_START'; - const atOrder = where === 'FROM_END' ? Order.UNARY : Order.NONE; - const at = generator.valueToCode(block, 'AT', atOrder) || '1'; const text = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; let code; if (where === 'RANDOM') { @@ -154,6 +142,8 @@ end } else if (where === 'LAST') { start = '-1'; } else { + const atOrder = where === 'FROM_END' ? Order.UNARY : Order.NONE; + const at = generator.valueToCode(block, 'AT', atOrder) || '1'; if (where === 'FROM_START') { start = at; } else if (where === 'FROM_END') { diff --git a/generators/php.ts b/generators/php.ts index 18631ad9a37..30a08086670 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 eefb7cba774..00000000000 --- 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/procedures.ts b/generators/php/procedures.ts index 5d44eb21bae..acf84aea658 100644 --- a/generators/php/procedures.ts +++ b/generators/php/procedures.ts @@ -27,7 +27,7 @@ export function procedures_defreturn(block: Block, generator: PhpGenerator) { for (const variable of usedVariables) { const varName = variable.name; // getVars returns parameter names, not ids, for procedure blocks - if (block.getVars().indexOf(varName) === -1) { + if (!block.getVars().includes(varName)) { globals.push(generator.getVariableName(varName)); } } @@ -60,8 +60,17 @@ export function procedures_defreturn(block: Block, generator: PhpGenerator) { generator.INDENT, ); } - const branch = generator.statementToCode(block, 'STACK'); - let returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + let branch = ''; + if (block.getInput('STACK')) { + // The 'procedures_defreturn' block might not have a STACK input. + branch = generator.statementToCode(block, 'STACK'); + } + let returnValue = ''; + if (block.getInput('RETURN')) { + // The 'procedures_defnoreturn' block (which shares this code) + // does not have a RETURN input. + returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + } let xfix2 = ''; if (branch && returnValue) { // After executing the function body, revisit this block for the return. diff --git a/generators/php/text.ts b/generators/php/text.ts index f2fa69faec3..811e0251fb3 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.indexOf('.') !== -1 ? 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 08ab10e81e1..d7b505763d7 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 729d87cf5f8..00000000000 --- 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/procedures.ts b/generators/python/procedures.ts index 0d9b2e85d90..51d2ee9a31b 100644 --- a/generators/python/procedures.ts +++ b/generators/python/procedures.ts @@ -27,7 +27,7 @@ export function procedures_defreturn(block: Block, generator: PythonGenerator) { for (const variable of usedVariables) { const varName = variable.name; // getVars returns parameter names, not ids, for procedure blocks - if (block.getVars().indexOf(varName) === -1) { + if (!block.getVars().includes(varName)) { globals.push(generator.getVariableName(varName)); } } @@ -60,8 +60,17 @@ export function procedures_defreturn(block: Block, generator: PythonGenerator) { generator.INDENT, ); } - let branch = generator.statementToCode(block, 'STACK'); - let returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + let branch = ''; + if (block.getInput('STACK')) { + // The 'procedures_defreturn' block might not have a STACK input. + branch = generator.statementToCode(block, 'STACK'); + } + let returnValue = ''; + if (block.getInput('RETURN')) { + // The 'procedures_defnoreturn' block (which shares this code) + // does not have a RETURN input. + returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + } let xfix2 = ''; if (branch && returnValue) { // After executing the function body, revisit this block for the return. diff --git a/generators/python/python_generator.ts b/generators/python/python_generator.ts index 229c2022ccb..bf7b87496ee 100644 --- a/generators/python/python_generator.ts +++ b/generators/python/python_generator.ts @@ -243,8 +243,8 @@ export class PythonGenerator extends CodeGenerator { // Follow the CPython behaviour of repr() for a non-byte string. let quote = "'"; - if (string.indexOf("'") !== -1) { - if (string.indexOf('"') === -1) { + if (string.includes("'")) { + if (!string.includes('"')) { quote = '"'; } else { string = string.replace(/'/g, "\\'"); @@ -276,7 +276,6 @@ export class PythonGenerator extends CodeGenerator { * @param code The Python code created for this block. * @param thisOnly True to generate code for only this statement. * @returns Python code with comments and subsequent blocks added. - */ scrub_(block: Block, code: string, thisOnly = false): string { let commentCode = ''; diff --git a/generators/python/text.ts b/generators/python/text.ts index 5d93e91ed21..e9154da83fe 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.indexOf('+') !== -1 ? 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 00000000000..1bd27ee735a --- /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 00000000000..7aeb5b17427 --- /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 00000000000..b46f177abb3 --- /dev/null +++ b/media/resize-handle.svg @@ -0,0 +1,3 @@ + + + diff --git a/msg/json/constants.json b/msg/json/constants.json index f2801d3556f..2677e406f74 100644 --- a/msg/json/constants.json +++ b/msg/json/constants.json @@ -1 +1,12 @@ -{"MATH_HUE": "230", "LOOPS_HUE": "120", "LISTS_HUE": "260", "LOGIC_HUE": "210", "VARIABLES_HUE": "330", "TEXTS_HUE": "160", "PROCEDURES_HUE": "290", "COLOUR_HUE": "20", "VARIABLES_DYNAMIC_HUE": "310"} \ No newline at end of file +{ + "#": "Automatically generated, do not edit this file!", + "COLOUR_HUE": "20", + "LISTS_HUE": "260", + "LOGIC_HUE": "210", + "LOOPS_HUE": "120", + "MATH_HUE": "230", + "PROCEDURES_HUE": "290", + "TEXTS_HUE": "160", + "VARIABLES_DYNAMIC_HUE": "310", + "VARIABLES_HUE": "330" +} \ No newline at end of file diff --git a/msg/json/de.json b/msg/json/de.json index eca87b60609..33ed7191066 100644 --- a/msg/json/de.json +++ b/msg/json/de.json @@ -14,6 +14,7 @@ "Sushi", "THINK", "Tiin", + "TomatoCake", "Zgtm", "아라" ] @@ -42,7 +43,7 @@ "REDO": "Wiederholen", "CHANGE_VALUE_TITLE": "Wert ändern:", "RENAME_VARIABLE": "Variable umbenennen …", - "RENAME_VARIABLE_TITLE": "Alle \"%1\"-Variablen umbenennen in:", + "RENAME_VARIABLE_TITLE": "Alle „%1“-Variablen umbenennen in:", "NEW_VARIABLE": "Variable erstellen …", "NEW_STRING_VARIABLE": "Zeichenfolgenvariable erstellen …", "NEW_NUMBER_VARIABLE": "Zahlenvariable erstellen …", @@ -51,6 +52,7 @@ "NEW_VARIABLE_TITLE": "Name der neuen Variable:", "VARIABLE_ALREADY_EXISTS": "Eine Variable namens „%1“ ist bereits vorhanden.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Eine Variable namens „%1“ ist bereits für einen anderen Typ vorhanden: „%2“.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Eine Variable mit dem Namen „%1“ ist bereits als Parameter in der Funktion „%2“ vorhanden.", "DELETE_VARIABLE_CONFIRMATION": "%1 Verwendungen der Variable „%2“ löschen?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Die Variable „%1“ kann nicht gelöscht werden, da sie Teil der Definition der Funktion „%2“ ist.", "DELETE_VARIABLE": "Die Variable „%1“ löschen", @@ -72,7 +74,7 @@ "CONTROLS_REPEAT_TITLE": "wiederhole %1-mal:", "CONTROLS_REPEAT_INPUT_DO": "mache", "CONTROLS_REPEAT_TOOLTIP": "Eine Anweisung mehrfach ausführen.", - "CONTROLS_WHILEUNTIL_HELPURL": "https://de.wikipedia.org/wiki/Schleife_%28Programmierung%29", + "CONTROLS_WHILEUNTIL_HELPURL": "https://de.wikipedia.org/wiki/Schleife_(Programmierung)#Kopfgesteuerte_Schleife", "CONTROLS_WHILEUNTIL_OPERATOR_WHILE": "wiederhole solange", "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "wiederhole bis", "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "Führt Anweisungen aus, solange die Bedingung wahr ist.", @@ -99,7 +101,7 @@ "CONTROLS_IF_IF_TOOLTIP": "Hinzufügen, entfernen oder sortieren von Sektionen", "CONTROLS_IF_ELSEIF_TOOLTIP": "Eine weitere Bedingung hinzufügen.", "CONTROLS_IF_ELSE_TOOLTIP": "Eine sonst-Bedingung hinzufügen. Führt eine Anweisung aus, falls keine Bedingung zutrifft.", - "LOGIC_COMPARE_HELPURL": "https://de.wikipedia.org/wiki/Vergleich_%28Zahlen%29", + "LOGIC_COMPARE_HELPURL": "https://de.wikipedia.org/wiki/Vergleich_(Zahlen)", "LOGIC_COMPARE_TOOLTIP_EQ": "Ist wahr, falls beide Werte gleich sind.", "LOGIC_COMPARE_TOOLTIP_NEQ": "Ist wahr, falls beide Werte unterschiedlich sind.", "LOGIC_COMPARE_TOOLTIP_LT": "Ist wahr, falls der erste Wert kleiner als der zweite Wert ist.", @@ -108,7 +110,7 @@ "LOGIC_COMPARE_TOOLTIP_GTE": "Ist wahr, falls der erste Wert größer als oder gleich groß wie der zweite Wert ist.", "LOGIC_OPERATION_TOOLTIP_AND": "Ist wahr, falls beide Werte wahr sind.", "LOGIC_OPERATION_AND": "und", - "LOGIC_OPERATION_TOOLTIP_OR": "Ist wahr, falls einer der beiden Werte wahr ist.", + "LOGIC_OPERATION_TOOLTIP_OR": "Ist wahr, falls mindestens einer der beiden Werte wahr ist.", "LOGIC_OPERATION_OR": "oder", "LOGIC_NEGATE_TITLE": "nicht %1", "LOGIC_NEGATE_TOOLTIP": "Ist wahr, falls der Eingabewert unwahr ist. Ist unwahr, falls der Eingabewert wahr ist.", @@ -140,8 +142,8 @@ "MATH_SINGLE_HELPURL": "https://de.wikipedia.org/wiki/Quadratwurzel", "MATH_SINGLE_OP_ROOT": "Quadratwurzel", "MATH_SINGLE_TOOLTIP_ROOT": "Ist die Quadratwurzel einer Zahl.", - "MATH_SINGLE_OP_ABSOLUTE": "Betrag", - "MATH_SINGLE_TOOLTIP_ABS": "Ist der Betrag einer Zahl.", + "MATH_SINGLE_OP_ABSOLUTE": "Absolutbetrag", + "MATH_SINGLE_TOOLTIP_ABS": "Ist der Absolutbetrag einer Zahl.", "MATH_SINGLE_TOOLTIP_NEG": "Negiert eine Zahl.", "MATH_SINGLE_TOOLTIP_LN": "Ist der natürliche Logarithmus einer Zahl.", "MATH_SINGLE_TOOLTIP_LOG10": "Ist der dekadische Logarithmus einer Zahl.", @@ -149,7 +151,7 @@ "MATH_SINGLE_TOOLTIP_POW10": "Rechnet 10 hoch eine Zahl.", "MATH_TRIG_HELPURL": "https://de.wikipedia.org/wiki/Trigonometrie", "MATH_TRIG_TOOLTIP_SIN": "Ist der Sinus des Winkels (nicht Radiant).", - "MATH_TRIG_TOOLTIP_COS": "Ist der Kosinus des Winkels.", + "MATH_TRIG_TOOLTIP_COS": "Ist der Kosinus des Winkels (nicht Radiant).", "MATH_TRIG_TOOLTIP_TAN": "Ist der Tangens des Winkels (nicht Radiant).", "MATH_TRIG_TOOLTIP_ASIN": "Ist der Arkussinus des Eingabewertes.", "MATH_TRIG_TOOLTIP_ACOS": "Ist der Arkuskosinus des Eingabewertes.", @@ -166,7 +168,7 @@ "MATH_IS_TOOLTIP": "Überprüft, ob eine Zahl gerade, ungerade, eine Primzahl, ganzzahlig, positiv, negativ oder durch eine zweite Zahl teilbar ist. Gibt wahr oder falsch zurück.", "MATH_CHANGE_HELPURL": "https://de.wikipedia.org/wiki/Inkrement_und_Dekrement", "MATH_CHANGE_TITLE": "erhöhe %1 um %2", - "MATH_CHANGE_TOOLTIP": "Addiert eine Zahl zu \"%1\".", + "MATH_CHANGE_TOOLTIP": "Addiert eine Zahl zur Variable „%1“.", "MATH_ROUND_HELPURL": "https://de.wikipedia.org/wiki/Runden", "MATH_ROUND_TOOLTIP": "Eine Zahl auf- oder abrunden.", "MATH_ROUND_OPERATOR_ROUND": "runde", @@ -216,7 +218,7 @@ "TEXT_LENGTH_TOOLTIP": "Die Anzahl von Zeichen in einem Text (inkl. Leerzeichen).", "TEXT_ISEMPTY_TITLE": "%1 ist leer", "TEXT_ISEMPTY_TOOLTIP": "Ist wahr, falls der Text keine Zeichen enthält.", - "TEXT_INDEXOF_TOOLTIP": "Findet das erste / letzte Auftreten eines Suchbegriffs in einem Text. Gibt die Position des Begriffs zurück oder %1 falls der Suchbegriff nicht gefunden wurde.", + "TEXT_INDEXOF_TOOLTIP": "Findet das erste / letzte Auftreten eines Suchbegriffs in einem Text. Gibt die Position des Begriffs zurück oder %1, falls der Suchbegriff nicht gefunden wurde.", "TEXT_INDEXOF_TITLE": "im Text %1 %2 %3", "TEXT_INDEXOF_OPERATOR_FIRST": "suche erstes Auftreten des Begriffs", "TEXT_INDEXOF_OPERATOR_LAST": "suche letztes Auftreten des Begriffs", @@ -264,7 +266,7 @@ "LISTS_CREATE_WITH_CONTAINER_TITLE_ADD": "Liste", "LISTS_CREATE_WITH_CONTAINER_TOOLTIP": "Hinzufügen, entfernen und sortieren von Elementen.", "LISTS_CREATE_WITH_ITEM_TOOLTIP": "Ein Element zur Liste hinzufügen.", - "LISTS_REPEAT_TOOLTIP": "Erzeugt eine Liste mit einer variablen Anzahl von Elementen.", + "LISTS_REPEAT_TOOLTIP": "Erzeugt eine Liste mit der angegebenen Anzahl an Elementen des angegebenen Wertes.", "LISTS_REPEAT_TITLE": "erzeuge Liste mit %2-mal dem Element %1", "LISTS_LENGTH_TITLE": "Länge von %1", "LISTS_LENGTH_TOOLTIP": "Die Anzahl von Elementen in der Liste.", @@ -279,9 +281,9 @@ "LISTS_GET_INDEX_REMOVE": "entferne", "LISTS_GET_INDEX_FROM_START": "das", "LISTS_GET_INDEX_FROM_END": "von hinten das", - "LISTS_GET_INDEX_FIRST": "Erste", - "LISTS_GET_INDEX_LAST": "Letzte", - "LISTS_GET_INDEX_RANDOM": "Zufällig", + "LISTS_GET_INDEX_FIRST": "das erste", + "LISTS_GET_INDEX_LAST": "das letzte", + "LISTS_GET_INDEX_RANDOM": "ein zufälliges", "LISTS_GET_INDEX_TAIL": "Element", "LISTS_INDEX_FROM_START_TOOLTIP": "%1 ist das erste Element.", "LISTS_INDEX_FROM_END_TOOLTIP": "%1 ist das letzte Element.", @@ -306,7 +308,7 @@ "LISTS_SET_INDEX_TOOLTIP_SET_RANDOM": "Setzt ein zufälliges Element in der Liste.", "LISTS_SET_INDEX_TOOLTIP_INSERT_FROM": "Fügt das Element an der angegebenen Position in die Liste ein.", "LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST": "Fügt das Element an den Anfang der Liste an.", - "LISTS_SET_INDEX_TOOLTIP_INSERT_LAST": "Fügt das Element ans Ende der Liste an.", + "LISTS_SET_INDEX_TOOLTIP_INSERT_LAST": "Fügt das Element an das Ende der Liste an.", "LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM": "Fügt das Element zufällig in die Liste ein.", "LISTS_GET_SUBLIST_START_FROM_START": "nimm Teilliste ab", "LISTS_GET_SUBLIST_START_FROM_END": "nimm Teilliste ab von hinten", @@ -348,9 +350,9 @@ "PROCEDURES_ALLOW_STATEMENTS": "Anweisungen erlauben", "PROCEDURES_DEF_DUPLICATE_WARNING": "Warnung: Dieser Funktionsblock hat zwei gleich benannte Parameter.", "PROCEDURES_CALLNORETURN_HELPURL": "https://de.wikipedia.org/wiki/Unterprogramm", - "PROCEDURES_CALLNORETURN_TOOLTIP": "Rufe einen Funktionsblock ohne Rückgabewert auf.", + "PROCEDURES_CALLNORETURN_TOOLTIP": "Ruft die benutzerdefinierte Funktion „%1“ auf.", "PROCEDURES_CALLRETURN_HELPURL": "https://de.wikipedia.org/wiki/Unterprogramm", - "PROCEDURES_CALLRETURN_TOOLTIP": "Rufe einen Funktionsblock mit Rückgabewert auf.", + "PROCEDURES_CALLRETURN_TOOLTIP": "Ruft die benutzerdefinierte Funktion „%1“ auf und verwendet ihre Ausgabe.", "PROCEDURES_MUTATORCONTAINER_TITLE": "Parameter", "PROCEDURES_MUTATORCONTAINER_TOOLTIP": "Die Eingaben zu dieser Funktion hinzufügen, entfernen oder neu anordnen.", "PROCEDURES_MUTATORARG_TITLE": "Variable:", @@ -360,8 +362,8 @@ "PROCEDURES_IFRETURN_TOOLTIP": "Gibt den zweiten Wert zurück und verlässt die Funktion, falls der erste Wert wahr ist.", "PROCEDURES_IFRETURN_WARNING": "Warnung: Dieser Block darf nur innerhalb eines Funktionsblocks genutzt werden.", "WORKSPACE_COMMENT_DEFAULT_TEXT": "Teile etwas mit…", - "WORKSPACE_ARIA_LABEL": "Blockly Arbeitsbereich", + "WORKSPACE_ARIA_LABEL": "Blockly-Arbeitsbereich", "COLLAPSED_WARNINGS_WARNING": "Eingeklappte Blöcke enthalten Warnungen.", - "DIALOG_OK": "OK", + "DIALOG_OK": "Okay", "DIALOG_CANCEL": "Abbrechen" } diff --git a/msg/json/el.json b/msg/json/el.json index 6d6638abf17..fe23891966f 100644 --- a/msg/json/el.json +++ b/msg/json/el.json @@ -3,6 +3,7 @@ "authors": [ "Azountas", "Ch.zarafidis", + "Dimitris131", "Espertus", "Evropi", "GR", @@ -32,75 +33,75 @@ "ADD_COMMENT": "Προσθήκη σχολίου", "REMOVE_COMMENT": "Αφαίρεση σχολίου", "DUPLICATE_COMMENT": "Αντιγραφή σχόλιου", - "EXTERNAL_INPUTS": "Εξωτερικές προσθήκες", - "INLINE_INPUTS": "Εσωτερικές προσθήκες", + "EXTERNAL_INPUTS": "Εξωτερικές είσοδοι", + "INLINE_INPUTS": "Ενσωματωμένες είσοδοι", "DELETE_BLOCK": "Διαγραφή μπλοκ", "DELETE_X_BLOCKS": "Διαγραφή %1 μπλοκ", "DELETE_ALL_BLOCKS": "Να διαγραφούν όλα τα %1 μπλοκ;", - "CLEAN_UP": "Ταξινομείστε τα Μπλοκ", - "COLLAPSE_BLOCK": "Σύμπτυξε Το Μπλοκ", - "COLLAPSE_ALL": "Σύμπτυξε Όλα Τα Μπλοκ", - "EXPAND_BLOCK": "Επέκτεινε Το Μπλοκ", - "EXPAND_ALL": "Επέκτεινε Όλα Τα Μπλοκ", - "DISABLE_BLOCK": "Απενεργοποίησε Το Μπλοκ", - "ENABLE_BLOCK": "Ενεργοποίησε Το Μπλοκ", + "CLEAN_UP": "Εκκαθάριση μπλοκ", + "COLLAPSE_BLOCK": "Σύμπτυξη μπλοκ", + "COLLAPSE_ALL": "Σύμπτυξη μπλοκ", + "EXPAND_BLOCK": "Ανάπτυξη μπλοκ", + "EXPAND_ALL": "Ανάπτυξη μπλοκ", + "DISABLE_BLOCK": "Απενεργοποίηση μπλοκ", + "ENABLE_BLOCK": "Ενεργοποίηση μπλοκ", "HELP": "Βοήθεια", "UNDO": "Αναίρεση", "REDO": "Ακύρωση αναίρεσης", "CHANGE_VALUE_TITLE": "Άλλαξε την τιμή:", "RENAME_VARIABLE": "Μετονόμασε τη μεταβλητή...", - "RENAME_VARIABLE_TITLE": "Μετονόμασε όλες τις μεταβλητές «%1» σε:", + "RENAME_VARIABLE_TITLE": "Μετονόμασε όλες τις μεταβλητές '%1' σε:", "NEW_VARIABLE": "Δημιουργήστε μεταβλητή...", - "NEW_STRING_VARIABLE": "Δημιουργία μεταβλητή συμβολοσειράς...", - "NEW_NUMBER_VARIABLE": "Δημιουργία μεταβλητής αριθμού...", - "NEW_COLOUR_VARIABLE": "Δημιουργία μεταβλητής χρώματος...", + "NEW_STRING_VARIABLE": "Δημιούργησε μεταβλητή συμβολοσειράς...", + "NEW_NUMBER_VARIABLE": "Δημιούργησε μεταβλητή αριθμού...", + "NEW_COLOUR_VARIABLE": "Δημιούργησε μεταβλητή χρώματος...", "NEW_VARIABLE_TYPE_TITLE": "Νέος τύπος μεταβλητής:", "NEW_VARIABLE_TITLE": "Νέο όνομα μεταβλητής:", "VARIABLE_ALREADY_EXISTS": "Η μεταβλητή με το όνομα '%1' υπάρχει ήδη.", - "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Μια μεταβλητή με όνομα '%1' υπάρχει πάντα για έναν άλλο τύπο: '%2'.", - "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Μια μεταβλητή με το όνομα «%1» υπάρχει ήδη ως παράμετρος στη διαδικασία «%2».", - "DELETE_VARIABLE_CONFIRMATION": "Θέλετε να διαγράψετε το %1 που χρησιμοποιείτε από την μεταβλητή '%2'?", - "CANNOT_DELETE_VARIABLE_PROCEDURE": "Δεν μπορώ να διαγράψω την μεταβλητή '%1' διότι είναι μέρος του ορισμού της λειτουργίας '%2'", - "DELETE_VARIABLE": "Διαγράψτε την μεταβλητή '%1'", - "COLOUR_PICKER_TOOLTIP": "Επιτρέπει επιλογή χρώματος από την παλέτα.", + "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Μια μεταβλητή με όνομα '%1' υπάρχει ήδη για έναν άλλο τύπο: '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Μια μεταβλητή με το όνομα '%1' υπάρχει ήδη ως παράμετρος στη διαδικασία '%2'.", + "DELETE_VARIABLE_CONFIRMATION": "Θέλετε να διαγράψετε %1 χρήσεις της μεταβλητής '%2'?", + "CANNOT_DELETE_VARIABLE_PROCEDURE": "Δεν μπορείς να διαγράψεις την μεταβλητή '%1' διότι είναι μέρος του ορισμού της συνάρτησης '%2'", + "DELETE_VARIABLE": "Διάγραψε την μεταβλητή '%1'", + "COLOUR_PICKER_TOOLTIP": "Επέλεξε ένα χρώμα από την παλέτα.", "COLOUR_RANDOM_TITLE": "τυχαίο χρώμα", - "COLOUR_RANDOM_TOOLTIP": "Επιλέγει χρώμα τυχαία.", + "COLOUR_RANDOM_TOOLTIP": "Επέλεξε ένα χρώμα τυχαία.", "COLOUR_RGB_TITLE": "χρώμα με", "COLOUR_RGB_RED": "κόκκινο", "COLOUR_RGB_GREEN": "πράσινο", "COLOUR_RGB_BLUE": "μπλε", - "COLOUR_RGB_TOOLTIP": "Δημιουργήστε ένα χρώμα με την καθορισμένη ποσότητα κόκκινου, πράσινου και μπλε. Όλες οι τιμές πρέπει να είναι μεταξύ 0 και 100.", + "COLOUR_RGB_TOOLTIP": "Δημιούργησε ένα χρώμα με την καθορισμένη ποσότητα κόκκινου, πράσινου και μπλε. Όλες οι τιμές πρέπει να είναι μεταξύ 0 και 100.", "COLOUR_BLEND_TITLE": "μείγμα", "COLOUR_BLEND_COLOUR1": "χρώμα 1", "COLOUR_BLEND_COLOUR2": "χρώμα 2", "COLOUR_BLEND_RATIO": "αναλογία", - "COLOUR_BLEND_TOOLTIP": "Συνδυάζει δύο χρώματα μαζί με μια δεδομένη αναλογία (0.0 - 1,0).", + "COLOUR_BLEND_TOOLTIP": "Συνδυάζει μαζί δύο χρώματα στην δοσμένη αναλογία (0.0 - 1.0).", "CONTROLS_REPEAT_TITLE": "επανάλαβε %1 φορές", "CONTROLS_REPEAT_INPUT_DO": "κάνε", - "CONTROLS_REPEAT_TOOLTIP": "Εκτελεί κάποιες εντολές αρκετές φορές.", + "CONTROLS_REPEAT_TOOLTIP": "Εκτέλεσε κάποιες εντολές αρκετές φορές.", "CONTROLS_WHILEUNTIL_OPERATOR_WHILE": "επανάλαβε ενώ", "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "επανάλαβε μέχρι", "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "Εφόσον μια τιμή είναι αληθής, τότε εκτελεί κάποιες εντολές.", "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "Εφόσον μια τιμή είναι ψευδής, τότε εκτελεί κάποιες εντολές.", - "CONTROLS_FOR_TOOLTIP": "Η μεταβλητή «%1» παίρνει τιμές ξεκινώντας από τον αριθμό έναρξης μέχρι τον αριθμό τέλους αυξάνοντας κάθε φορά με το καθορισμένο βήμα και εκτελώντας το καθορισμένο μπλοκ.", + "CONTROLS_FOR_TOOLTIP": "Βάλε τη μεταβλητή '%1' να πάρει τιμές από τον αριθμό έναρξης μέχρι τον αριθμό λήξης αυξάνοντας κάθε φορά με το καθορισμένο βήμα και εκτελώντας τα καθορισμένα μπλοκ.", "CONTROLS_FOR_TITLE": "μέτρησε με %1 από το %2 έως το %3 ανά %4", "CONTROLS_FOREACH_TITLE": "για κάθε στοιχείο %1 στη λίστα %2", - "CONTROLS_FOREACH_TOOLTIP": "Για κάθε στοιχείο σε μια λίστα, ορίζει τη μεταβλητή «%1» στο στοιχείο και, στη συνέχεια, εκτελεί κάποιες εντολές.", - "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "φεύγει από το μπλοκ επαναλήψεως", + "CONTROLS_FOREACH_TOOLTIP": "Για κάθε στοιχείο σε μια λίστα, όρισε τη μεταβλητή '%1' στο στοιχείο και, στη συνέχεια, εκτέλεσε κάποιες εντολές.", + "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "φύγε από το μπλοκ επαναλήψεως", "CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE": "συνέχισε με την επόμενη επανάληψη του μπλοκ επαναλήψεως", - "CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK": "Ξεφεύγει (βγαίνει έξω) από την επανάληψη.", - "CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE": "Παραλείπει το υπόλοιπο τμήμα αυτού του μπλοκ επαναλήψεως, και συνεχίζει με την επόμενη επανάληψη.", + "CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK": "Φύγε από την περιεχόμενη επανάληψη.", + "CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE": "Παρέλειψε το υπόλοιπο αυτού του μπλοκ επαναλήψεως, και συνέχισε με την επόμενη επανάληψη.", "CONTROLS_FLOW_STATEMENTS_WARNING": "Προειδοποίηση: Αυτό το μπλοκ μπορεί να χρησιμοποιηθεί μόνο μέσα σε μια επανάληψη.", - "CONTROLS_IF_TOOLTIP_1": "Αν μια τιμή είναι αληθής, τότε εκτελεί κάποιες εντολές.", - "CONTROLS_IF_TOOLTIP_2": "Αν μια τιμή είναι αληθής, τότε εκτελεί το πρώτο τμήμα εντολών. Διαφορετικά, εκτελεί το δεύτερο τμήμα εντολών.", - "CONTROLS_IF_TOOLTIP_3": "Αν η πρώτη τιμή είναι αληθής, τότε εκτελεί το πρώτο τμήμα εντολών. Διαφορετικά, αν η δεύτερη τιμή είναι αληθής, εκτελεί το δεύτερο μπλοκ εντολών.", - "CONTROLS_IF_TOOLTIP_4": "Αν η πρώτη τιμή είναι αληθής, τότε εκτελεί το πρώτο τμήμα εντολών. Διαφορετικά, αν η δεύτερη τιμή είναι αληθής, εκτελεί το δεύτερο τμήμα εντολών. Αν καμία από τις τιμές δεν είναι αληθής, εκτελεί το τελευταίο τμήμα εντολών.", + "CONTROLS_IF_TOOLTIP_1": "Αν μια τιμή είναι αληθής, τότε εκτέλεσε κάποιες εντολές.", + "CONTROLS_IF_TOOLTIP_2": "Αν μια τιμή είναι αληθής, τότε εκτέλεσε το πρώτο μπλοκ εντολών. Διαφορετικά, εκτέλεσε το δεύτερο μπλοκ εντολών.", + "CONTROLS_IF_TOOLTIP_3": "Αν η πρώτη τιμή είναι αληθής, τότε εκτέλεσε το πρώτο τμήμα εντολών. Διαφορετικά, αν η δεύτερη τιμή είναι αληθής, εκτέλεσε το δεύτερο μπλοκ εντολών.", + "CONTROLS_IF_TOOLTIP_4": "Αν η πρώτη τιμή είναι αληθής, τότε εκτέλεσε το πρώτο μπλοκ εντολών. Διαφορετικά, αν η δεύτερη τιμή είναι αληθής, εκτέλεσε το δεύτερο μπλοκ εντολών. Αν καμία από τις τιμές δεν είναι αληθής, εκτέλεσε το τελευταίο τμήμα εντολών.", "CONTROLS_IF_MSG_IF": "εάν", "CONTROLS_IF_MSG_ELSEIF": "εναλλακτικά εάν", - "CONTROLS_IF_MSG_ELSE": "αλλιώς", + "CONTROLS_IF_MSG_ELSE": "εναλλακτικά", "CONTROLS_IF_IF_TOOLTIP": "Προσθέτει, αφαιρεί ή αναδιατάσσει τα τμήματα για να αναδιαμορφώσει αυτό το μπλοκ «εάν».", - "CONTROLS_IF_ELSEIF_TOOLTIP": "Πρόσθετει μια κατάσταση/συνθήκη στο μπλοκ «εάν».", - "CONTROLS_IF_ELSE_TOOLTIP": "Προσθέτει μια τελική κατάσταση/συνθήκη, που πιάνει όλες τις άλλες περιπτώσεις, στο μπλοκ «εάν».", + "CONTROLS_IF_ELSEIF_TOOLTIP": "Πρόσθεσε μια συνθήκη στο μπλοκ «εάν».", + "CONTROLS_IF_ELSE_TOOLTIP": "Πρόσθεσε μια τελική συνθήκη, που πιάνει όλες τις άλλες περιπτώσεις, στο μπλοκ «εάν».", "LOGIC_COMPARE_TOOLTIP_EQ": "Επιστρέφει αληθής αν και οι δύο είσοδοι είναι ίσες μεταξύ τους.", "LOGIC_COMPARE_TOOLTIP_NEQ": "Επιστρέφει αληθής αν και οι δύο είσοδοι δεν είναι ίσες μεταξύ τους.", "LOGIC_COMPARE_TOOLTIP_LT": "Επιστρέφει αληθής αν η πρώτη είσοδος είναι μικρότερη από τη δεύτερη είσοδο.", @@ -121,7 +122,7 @@ "LOGIC_TERNARY_CONDITION": "έλεγχος", "LOGIC_TERNARY_IF_TRUE": "εάν είναι αληθής", "LOGIC_TERNARY_IF_FALSE": "εάν είναι ψευδής", - "LOGIC_TERNARY_TOOLTIP": "Ελέγχει την κατάσταση/συνθήκη στον «έλεγχο». Αν η κατάσταση/συνθήκη είναι αληθής, επιστρέφει την τιμή «εάν αληθής», διαφορετικά επιστρέφει την τιμή «εάν ψευδής».", + "LOGIC_TERNARY_TOOLTIP": "Ελέγχει την συνθήκη στον «έλεγχο». Αν η συνθήκη είναι αληθής, επιστρέφει την τιμή 'εάν αληθής', διαφορετικά επιστρέφει την τιμή 'εάν ψευδής'.", "MATH_NUMBER_HELPURL": "https://el.wikipedia.org/wiki/%CE%91%CF%81%CE%B9%CE%B8%CE%BC%CF%8C%CF%82", "MATH_NUMBER_TOOLTIP": "Ένας αριθμός.", "MATH_POWER_SYMBOL": "^ ύψωση σε δύναμη", @@ -142,15 +143,15 @@ "MATH_SINGLE_TOOLTIP_ROOT": "Επιστρέφει την τετραγωνική ρίζα ενός αριθμού.", "MATH_SINGLE_OP_ABSOLUTE": "απόλυτη", "MATH_SINGLE_TOOLTIP_ABS": "Επιστρέφει την απόλυτη τιμή ενός αριθμού.", - "MATH_SINGLE_TOOLTIP_NEG": "Επιστρέφει την αρνητική ενός αριθμού.", - "MATH_SINGLE_TOOLTIP_LN": "Επιστρέφει τον νεπέρειο λογάριθμο ενός αριθμού.", + "MATH_SINGLE_TOOLTIP_NEG": "Επιστρέφει την αρνητική τιμή ενός αριθμού.", + "MATH_SINGLE_TOOLTIP_LN": "Επιστρέφει τον φυσικό λογάριθμο ενός αριθμού.", "MATH_SINGLE_TOOLTIP_LOG10": "Επιστρέφει τον λογάριθμο με βάση το 10 ενός αριθμού.", "MATH_SINGLE_TOOLTIP_EXP": "Επιστρέφει το e υψωμένο στη δύναμη ενός αριθμού.", "MATH_SINGLE_TOOLTIP_POW10": "Επιστρέφει το 10 υψωμένο στη δύναμη ενός αριθμού.", "MATH_TRIG_HELPURL": "https://el.wikipedia.org/wiki/%CE%A4%CF%81%CE%B9%CE%B3%CF%89%CE%BD%CE%BF%CE%BC%CE%B5%CF%84%CF%81%CE%B9%CE%BA%CE%AE_%CF%83%CF%85%CE%BD%CE%AC%CF%81%CF%84%CE%B7%CF%83%CE%B7", - "MATH_TRIG_TOOLTIP_SIN": "Επιστρέφει το ημίτονο ενός βαθμού (όχι ακτινίου).", - "MATH_TRIG_TOOLTIP_COS": "Επιστρέφει το συνημίτονο ενός βαθμού (όχι ακτινίου).", - "MATH_TRIG_TOOLTIP_TAN": "Επιστρέφει την εφαπτομένη ενός βαθμού (όχι ακτινίου).", + "MATH_TRIG_TOOLTIP_SIN": "Επιστρέφει το ημίτονο μία γωνίας σε μοίρες (όχι ακτίνια).", + "MATH_TRIG_TOOLTIP_COS": "Επιστρέφει το συνημίτονο μίας γωνίας σε μοίρες (όχι ακτίνια).", + "MATH_TRIG_TOOLTIP_TAN": "Επιστρέφει την εφαπτομένη μία γωνίας σε μοίρες (όχι ακτίνια).", "MATH_TRIG_TOOLTIP_ASIN": "Επιστρέφει το τόξο ημίτονου ενός αριθμού.", "MATH_TRIG_TOOLTIP_ACOS": "Επιστρέφει το τόξο συνημίτονου ενός αριθμού.", "MATH_TRIG_TOOLTIP_ATAN": "Επιστρέφει το τόξο εφαπτομένης ενός αριθμού.", @@ -165,8 +166,8 @@ "MATH_IS_TOOLTIP": "Ελέγχει αν ένας αριθμός είναι άρτιος, περιττός, πρώτος, ακέραιος, θετικός, αρνητικός, ή αν είναι διαιρετός από έναν ορισμένο αριθμό. Επιστρέφει αληθής ή ψευδής.", "MATH_CHANGE_HELPURL": "https://el.wikipedia.org/wiki/%CE%A0%CF%81%CF%8C%CF%83%CE%B8%CE%B5%CF%83%CE%B7", "MATH_CHANGE_TITLE": "άλλαξε %1 αυξάνοντας κατά %2", - "MATH_CHANGE_TOOLTIP": "Προσθέτει έναν αριθμό στη μεταβλητή «%1».", - "MATH_ROUND_TOOLTIP": "Στρογγυλοποιεί έναν αριθμό προς τα πάνω ή προς τα κάτω.", + "MATH_CHANGE_TOOLTIP": "Πρόσθεσε έναν αριθμό στη μεταβλητή '%1'.", + "MATH_ROUND_TOOLTIP": "Στρογγυλοποίησε έναν αριθμό προς τα πάνω ή προς τα κάτω.", "MATH_ROUND_OPERATOR_ROUND": "στρογγυλοποίησε", "MATH_ROUND_OPERATOR_ROUNDUP": "στρογγυλοποίησε προς τα πάνω", "MATH_ROUND_OPERATOR_ROUNDDOWN": "στρογγυλοποίησε προς τα κάτω", @@ -179,9 +180,9 @@ "MATH_ONLIST_OPERATOR_AVERAGE": "μέσος όρος λίστας", "MATH_ONLIST_TOOLTIP_AVERAGE": "Επιστρέφει τον αριθμητικό μέσο όρο από τις αριθμητικές τιμές στη λίστα.", "MATH_ONLIST_OPERATOR_MEDIAN": "διάμεσος λίστας", - "MATH_ONLIST_TOOLTIP_MEDIAN": "Επιστρέφει τον διάμεσο της λίστας.", - "MATH_ONLIST_OPERATOR_MODE": "μορφές λίστας", - "MATH_ONLIST_TOOLTIP_MODE": "Επιστρέφει μια λίστα με τα πιο κοινά στοιχεία στη λίστα.", + "MATH_ONLIST_TOOLTIP_MEDIAN": "Επιστρέφει τον διάμεσο αριθμό της λίστας.", + "MATH_ONLIST_OPERATOR_MODE": "επικρατούσες τιμές λίστας", + "MATH_ONLIST_TOOLTIP_MODE": "Επιστρέφει μια λίστα με τα πιο συνηθισμένα στοιχεία της λίστας, δηλαδή τις επικρατούσες τιμές.", "MATH_ONLIST_OPERATOR_STD_DEV": "τυπική απόκλιση λίστας", "MATH_ONLIST_TOOLTIP_STD_DEV": "Επιστρέφει την τυπική απόκλιση της λίστας.", "MATH_ONLIST_OPERATOR_RANDOM": "τυχαίο στοιχείο λίστας", @@ -189,28 +190,28 @@ "MATH_MODULO_TITLE": "υπόλοιπο της %1 ÷ %2", "MATH_MODULO_TOOLTIP": "Επιστρέφει το υπόλοιπο της διαίρεσης των δύο αριθμών.", "MATH_CONSTRAIN_TITLE": "περιόρισε %1 χαμηλή %2 υψηλή %3", - "MATH_CONSTRAIN_TOOLTIP": "Περιορίζει έναν αριθμό μεταξύ των προβλεπόμενων ορίων (χωρίς αποκλεισμούς).", + "MATH_CONSTRAIN_TOOLTIP": "Περιορίζει έναν αριθμό μεταξύ των προβλεπόμενων ορίων (συμπεριλαμβανομένων και των ακραίων τιμών).", "MATH_RANDOM_INT_TITLE": "τυχαίος ακέραιος από το %1 έως το %2", - "MATH_RANDOM_INT_TOOLTIP": "Επιστρέφει έναν τυχαίο ακέραιο αριθμό μεταξύ δύο συγκεκριμένων ορίων (εντός - συμπεριλαμβανομένων και των ακραίων τιμών).", + "MATH_RANDOM_INT_TOOLTIP": "Επέστρεψε έναν τυχαίο ακέραιο αριθμό μεταξύ δύο συγκεκριμένων ορίων (συμπεριλαμβανομένων και των ακραίων τιμών).", "MATH_RANDOM_FLOAT_HELPURL": "https://el.wikipedia.org/wiki/%CE%93%CE%B5%CE%BD%CE%BD%CE%AE%CF%84%CF%81%CE%B9%CE%B1_%CE%A4%CF%85%CF%87%CE%B1%CE%AF%CF%89%CE%BD_%CE%91%CF%81%CE%B9%CE%B8%CE%BC%CF%8E%CE%BD", "MATH_RANDOM_FLOAT_TITLE_RANDOM": "τυχαίο κλάσμα", - "MATH_RANDOM_FLOAT_TOOLTIP": "Επιστρέψει ένα τυχαία κλάσμα μεταξύ 0,0 (κλειστό) και 1,0 (ανοικτό).", - "MATH_ATAN2_TITLE": "atan2 από X:%1 Y:%2", - "MATH_ATAN2_TOOLTIP": "Επιστρέφει την διαφορά τόξου των σημείων (X, Y) σε μοίρες από -180 σε 180.", + "MATH_RANDOM_FLOAT_TOOLTIP": "Επιστρέψει ένα τυχαία κλάσμα μεταξύ 0.0 (συμπεριλαμβανομένου) και 1.0 (χωρίς).", + "MATH_ATAN2_TITLE": "atan2 του X:%1 Y:%2", + "MATH_ATAN2_TOOLTIP": "Επιστρέφει το τόξο εφαπτομένης του σημείου (X, Y) σε μοίρες από -180 σε 180.", "TEXT_TEXT_HELPURL": "https://el.wikipedia.org/wiki/%CE%A3%CF%85%CE%BC%CE%B2%CE%BF%CE%BB%CE%BF%CF%83%CE%B5%CE%B9%CF%81%CE%AC", "TEXT_TEXT_TOOLTIP": "Ένα γράμμα, μια λέξη ή μια γραμμή κειμένου.", "TEXT_JOIN_TITLE_CREATEWITH": "δημιούργησε κείμενο με", - "TEXT_JOIN_TOOLTIP": "Δημιουργεί ένα κομμάτι κειμένου ενώνοντας έναν απεριόριστο αριθμό αντικειμένων.", + "TEXT_JOIN_TOOLTIP": "Δημιούργησε ένα κομμάτι κειμένου ενώνοντας έναν απεριόριστο αριθμό αντικειμένων.", "TEXT_CREATE_JOIN_TITLE_JOIN": "ένωσε", - "TEXT_CREATE_JOIN_TOOLTIP": "Προσθέτει, αφαιρεί ή αναδιατάσσει τους τομείς για να αναδιαμορφώσει αυτό το μπλοκ κειμένου.", - "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "Προσθέτει ένα στοιχείο στο κείμενο.", - "TEXT_APPEND_TITLE": "έως %1 ανάθεσε κείμενο %2", - "TEXT_APPEND_TOOLTIP": "Αναθέτει κείμενο στη μεταβλητή «%1».", - "TEXT_LENGTH_TITLE": "το μήκος του %1", - "TEXT_LENGTH_TOOLTIP": "Επιστρέφει το πλήθος των γραμμάτων (συμπεριλαμβανομένων και των κενών διαστημάτων) στο παρεχόμενο κείμενο.", + "TEXT_CREATE_JOIN_TOOLTIP": "Πρόσθεσε, αφαίρεσε ή αναδιάταξε τα τμήματα για να αναδιαμορφώσεις αυτό το μπλοκ κειμένου.", + "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "Πρόσθεσε ένα στοιχείο στο κείμενο.", + "TEXT_APPEND_TITLE": "στο %1 πρόσθεσε το κείμενο %2", + "TEXT_APPEND_TOOLTIP": "Πρόσθεσε κάποιο κείμενο στη μεταβλητή '%1'.", + "TEXT_LENGTH_TITLE": "μήκος του %1", + "TEXT_LENGTH_TOOLTIP": "Επιστρέφει το πλήθος των γραμμάτων (συμπεριλαμβανομένων και των κενών διαστημάτων) στο δοσμένο κείμενο.", "TEXT_ISEMPTY_TITLE": "το %1 είναι κενό", - "TEXT_ISEMPTY_TOOLTIP": "Επιστρέφει αληθής αν το παρεχόμενο κείμενο είναι κενό.", - "TEXT_INDEXOF_TOOLTIP": "Επιστρέφει τον δείκτη της πρώτης/τελευταίας εμφάνισης του πρώτου κειμένου στο δεύτερο κείμενο. Επιστρέφει τιμή %1, αν δε βρει το κείμενο.", + "TEXT_ISEMPTY_TOOLTIP": "Επιστρέφει αληθής αν το δοσμένο κείμενο είναι κενό.", + "TEXT_INDEXOF_TOOLTIP": "Επιστρέφει τον δείκτη της πρώτης/τελευταίας εμφάνισης του πρώτου κειμένου στο δεύτερο κείμενο. Επιστρέφει %1, αν δε βρει το κείμενο.", "TEXT_INDEXOF_TITLE": "στο κείμενο %1 %2 %3", "TEXT_INDEXOF_OPERATOR_FIRST": "βρες την πρώτη εμφάνιση του κειμένου", "TEXT_INDEXOF_OPERATOR_LAST": "βρες την τελευταία εμφάνιση του κειμένου", @@ -223,49 +224,49 @@ "TEXT_CHARAT_TOOLTIP": "Επιστρέφει το γράμμα στην καθορισμένη θέση.", "TEXT_GET_SUBSTRING_TOOLTIP": "Επιστρέφει ένα συγκεκριμένο τμήμα του κειμένου.", "TEXT_GET_SUBSTRING_INPUT_IN_TEXT": "στο κείμενο", - "TEXT_GET_SUBSTRING_START_FROM_START": "πάρε τη δευτερεύουσα συμβολοσειρά από το # γράμμα", - "TEXT_GET_SUBSTRING_START_FROM_END": "πάρε τη δευτερεύουσα συμβολοσειρά από το # γράμμα από το τέλος", - "TEXT_GET_SUBSTRING_START_FIRST": "πάρε τη δευτερεύουσα συμβολοσειρά από το πρώτο γράμμα", + "TEXT_GET_SUBSTRING_START_FROM_START": "πάρε τη υποσυμβολοσειρά από το γράμμα #", + "TEXT_GET_SUBSTRING_START_FROM_END": "πάρε την υποσυμβολοσειρά από το # γράμμα από το τέλος", + "TEXT_GET_SUBSTRING_START_FIRST": "πάρε την υποσυμβολοσειρά από το πρώτο γράμμα", "TEXT_GET_SUBSTRING_END_FROM_START": "μέχρι το # γράμμα", "TEXT_GET_SUBSTRING_END_FROM_END": "μέχρι το # γράμμα από το τέλος", "TEXT_GET_SUBSTRING_END_LAST": "μέχρι το τελευταίο γράμμα", - "TEXT_CHANGECASE_TOOLTIP": "Επιστρέφει ένα αντίγραφο του κειμένου σε διαφορετική μορφή γραμμάτων.", + "TEXT_CHANGECASE_TOOLTIP": "Επιστρέφει ένα αντίγραφο του κειμένου σε διαφορετική μορφή γραμμάτων (πεζά/κεφαλαία).", "TEXT_CHANGECASE_OPERATOR_UPPERCASE": "σε ΚΕΦΑΛΑΙΑ", "TEXT_CHANGECASE_OPERATOR_LOWERCASE": "σε πεζά", "TEXT_CHANGECASE_OPERATOR_TITLECASE": "σε Λέξεις Με Πρώτα Κεφαλαία", "TEXT_TRIM_TOOLTIP": "Επιστρέφει ένα αντίγραφο του κειμένου με αφαιρεμένα τα κενά από το ένα ή και τα δύο άκρα.", - "TEXT_TRIM_OPERATOR_BOTH": "περίκοψε τα κενά και από τις δυο πλευρές του", - "TEXT_TRIM_OPERATOR_LEFT": "περίκοψε τα κενά από την αριστερή πλευρά του", - "TEXT_TRIM_OPERATOR_RIGHT": "περίκοψε τα κενά από την δεξιά πλευρά του", + "TEXT_TRIM_OPERATOR_BOTH": "Αφαίρεσε τα κενά και από τις δυο πλευρές του", + "TEXT_TRIM_OPERATOR_LEFT": "Αφαίρεσε τα κενά από την αριστερή πλευρά του", + "TEXT_TRIM_OPERATOR_RIGHT": "Αφαίρεσε τα κενά από την δεξιά πλευρά του", "TEXT_PRINT_TITLE": "εκτύπωσε %1", "TEXT_PRINT_TOOLTIP": "Εκτυπώνει το καθορισμένο κείμενο, αριθμό ή άλλη τιμή.", - "TEXT_PROMPT_TYPE_TEXT": "πρότρεψε με μήνυμα για να δοθεί κείμενο", - "TEXT_PROMPT_TYPE_NUMBER": "πρότρεψε με μήνυμα για να δοθεί αριθμός", - "TEXT_PROMPT_TOOLTIP_NUMBER": "Δημιουργεί προτροπή για τον χρήστη για να δώσει ένα αριθμό.", + "TEXT_PROMPT_TYPE_TEXT": "προτροπή για κείμενο με μήνυμα", + "TEXT_PROMPT_TYPE_NUMBER": "προτροπή για αριθμό με μήνυμα", + "TEXT_PROMPT_TOOLTIP_NUMBER": "Δημιουργεί προτροπή για τον χρήστη για να δώσει έναν αριθμό.", "TEXT_PROMPT_TOOLTIP_TEXT": "Δημιουργεί προτροπή για το χρήστη για να δώσει κάποιο κείμενο.", - "TEXT_COUNT_MESSAGE0": "αριθμός %1 σε %2", - "TEXT_COUNT_TOOLTIP": "Μετρά πόσες φορές κάποιο κείμενο εμφανίζεται μέσα σε ένα άλλο κείμενο.", - "TEXT_REPLACE_MESSAGE0": "αντικαταστήσετε το %1 με %2 σε %3", - "TEXT_REPLACE_TOOLTIP": "Αντικαταστήστε όλα τα ήδη υπάρχοντα στοιχεία μέρους του κειμένου με κάποιο άλλο κείμενο", - "TEXT_REVERSE_MESSAGE0": "ανάκληση %1", - "TEXT_REVERSE_TOOLTIP": "Αναγραμματισμός των χαρακτήρων του κειμένου", + "TEXT_COUNT_MESSAGE0": "πλήθος %1 στο %2", + "TEXT_COUNT_TOOLTIP": "Μέτρα πόσες φορές κάποιο κείμενο εμφανίζεται μέσα σε ένα άλλο κείμενο.", + "TEXT_REPLACE_MESSAGE0": "αντικατάσταση του %1 με %2 στο %3", + "TEXT_REPLACE_TOOLTIP": "Αντικατάσταση όλων των εμφανίσεων ενός κειμένου μέσα σε κάποιο άλλο κείμενο", + "TEXT_REVERSE_MESSAGE0": "αντιστροφή %1", + "TEXT_REVERSE_TOOLTIP": "Αντιστρέφει τη σειρά των χαρακτήρων του κειμένου.", "LISTS_CREATE_EMPTY_TITLE": "δημιούργησε κενή λίστα", - "LISTS_CREATE_EMPTY_TOOLTIP": "Επιστρέφει μια λίστα, με μήκος 0, η οποία δεν περιέχει εγγραφές δεδομένων", + "LISTS_CREATE_EMPTY_TOOLTIP": "Επιστρέφει μια λίστα, με μήκος 0, η οποία δεν έχει στοιχεία", "LISTS_CREATE_WITH_TOOLTIP": "Δημιουργεί λίστα με οποιονδήποτε αριθμό αντικειμένων.", "LISTS_CREATE_WITH_INPUT_WITH": "δημιούργησε λίστα με", "LISTS_CREATE_WITH_CONTAINER_TITLE_ADD": "λίστα", - "LISTS_CREATE_WITH_CONTAINER_TOOLTIP": "Προσθέτει, αφαιρεί ή αναδιατάσσει τα τμήματα για να αναδιαμορφώσει αυτό το μπλοκ λίστας.", - "LISTS_CREATE_WITH_ITEM_TOOLTIP": "Προσθέτει αντικείμενο στη λίστα.", - "LISTS_REPEAT_TOOLTIP": "Δημιουργεί μια λίστα που αποτελείται από την δεδομένη τιμή που επαναλαμβάνεται για συγκεκριμένο αριθμό επαναλήψεων.", + "LISTS_CREATE_WITH_CONTAINER_TOOLTIP": "Πρόσθεσε, αφαίρεσε ή αναδιάταξε τμήματα για να αναδιαμορφώσει αυτό το μπλοκ λίστας.", + "LISTS_CREATE_WITH_ITEM_TOOLTIP": "Πρόσθεσε ένα αντικείμενο στη λίστα.", + "LISTS_REPEAT_TOOLTIP": "Δημιουργεί μια λίστα που αποτελείται από την δοσμένη τιμή που επαναλαμβάνεται για έναν δοσμένο αριθμό φορών.", "LISTS_REPEAT_TITLE": "δημιούργησε λίστα με το στοιχείο %1 να επαναλαμβάνεται %2 φορές", "LISTS_LENGTH_TITLE": "το μήκος του %1", - "LISTS_LENGTH_TOOLTIP": "Επιστρέφει το μήκος μιας λίστας.", + "LISTS_LENGTH_TOOLTIP": "Επιστρέφει το μήκος μίας λίστας.", "LISTS_ISEMPTY_TITLE": "το %1 είναι κενό", "LISTS_ISEMPTY_TOOLTIP": "Επιστρέφει αληθής αν η λίστα είναι κενή.", "LISTS_INLIST": "στη λίστα", "LISTS_INDEX_OF_FIRST": "βρες την πρώτη εμφάνιση του στοιχείου", "LISTS_INDEX_OF_LAST": "βρες την τελευταία εμφάνιση του στοιχείου", - "LISTS_INDEX_OF_TOOLTIP": "Επιστρέφει τον δείκτη της πρώτης/τελευταίας εμφάνισης του στοιχείου στη λίστα. Επιστρέφει τιμή %1, αν το στοιχείο δεν βρεθεί.", + "LISTS_INDEX_OF_TOOLTIP": "Επιστρέφει τον δείκτη της πρώτης/τελευταίας εμφάνισης του στοιχείου στη λίστα. Επιστρέφει %1, αν το στοιχείο δεν βρεθεί.", "LISTS_GET_INDEX_GET": "πάρε", "LISTS_GET_INDEX_GET_REMOVE": "πάρε και αφαίρεσε", "LISTS_GET_INDEX_REMOVE": "αφαίρεσε", @@ -276,28 +277,28 @@ "LISTS_GET_INDEX_RANDOM": "τυχαίο", "LISTS_INDEX_FROM_START_TOOLTIP": "Το %1 είναι το πρώτο στοιχείο.", "LISTS_INDEX_FROM_END_TOOLTIP": "Το %1 είναι το τελευταίο στοιχείο.", - "LISTS_GET_INDEX_TOOLTIP_GET_FROM": "Επιστρέφει το στοιχείο στην καθορισμένη θέση σε μια λίστα.", + "LISTS_GET_INDEX_TOOLTIP_GET_FROM": "Επιστρέφει το στοιχείο στην δοσμένη θέση σε μια λίστα.", "LISTS_GET_INDEX_TOOLTIP_GET_FIRST": "Επιστρέφει το πρώτο στοιχείο σε μια λίστα.", "LISTS_GET_INDEX_TOOLTIP_GET_LAST": "Επιστρέφει το τελευταίο στοιχείο σε μια λίστα.", "LISTS_GET_INDEX_TOOLTIP_GET_RANDOM": "Επιστρέφει ένα τυχαίο στοιχείο σε μια λίστα.", - "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FROM": "Καταργεί και επιστρέφει το στοιχείο στην καθορισμένη θέση σε μια λίστα.", - "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FIRST": "Καταργεί και επιστρέφει το πρώτο στοιχείο σε μια λίστα.", - "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_LAST": "Καταργεί και επιστρέφει το τελευταίο στοιχείο σε μια λίστα.", + "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FROM": "Αφαιρεί και επιστρέφει το στοιχείο στην καθορισμένη θέση σε μια λίστα.", + "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FIRST": "Αφαιρεί και επιστρέφει το πρώτο στοιχείο σε μια λίστα.", + "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_LAST": "Αφαιρεί και επιστρέφει το τελευταίο στοιχείο σε μια λίστα.", "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_RANDOM": "Καταργεί και επιστρέφει ένα τυχαίο στοιχείο σε μια λίστα.", - "LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM": "Καταργεί το στοιχείο στην καθορισμένη θέση σε μια λίστα.", + "LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM": "Αφαιρεί το στοιχείο στην καθορισμένη θέση σε μια λίστα.", "LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST": "Καταργεί το πρώτο στοιχείο σε μια λίστα.", "LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST": "Καταργεί το τελευταίο στοιχείο σε μια λίστα.", - "LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM": "Καταργεί ένα τυχαίο στοιχείο σε μια λίστα.", + "LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM": "Αφαιρεί ένα τυχαίο στοιχείο σε μια λίστα.", "LISTS_SET_INDEX_SET": "όρισε", "LISTS_SET_INDEX_INSERT": "είσαγε στο", - "LISTS_SET_INDEX_INPUT_TO": "σε", - "LISTS_SET_INDEX_TOOLTIP_SET_FROM": "Ορίζει το στοιχείο στην καθορισμένη θέση σε μια λίστα.", + "LISTS_SET_INDEX_INPUT_TO": "ως", + "LISTS_SET_INDEX_TOOLTIP_SET_FROM": "Ορίζει το στοιχείο στην δοσμένη θέση σε μια λίστα.", "LISTS_SET_INDEX_TOOLTIP_SET_FIRST": "Ορίζει το πρώτο στοιχείο σε μια λίστα.", "LISTS_SET_INDEX_TOOLTIP_SET_LAST": "Ορίζει το τελευταίο στοιχείο σε μια λίστα.", "LISTS_SET_INDEX_TOOLTIP_SET_RANDOM": "Ορίζει ένα τυχαίο στοιχείο σε μια λίστα.", - "LISTS_SET_INDEX_TOOLTIP_INSERT_FROM": "Εισάγει το στοιχείο στην καθορισμένη θέση σε μια λίστα.", + "LISTS_SET_INDEX_TOOLTIP_INSERT_FROM": "Εισάγει το στοιχείο στην δοσμένη θέση σε μια λίστα.", "LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST": "Εισάγει το στοιχείο στην αρχή μιας λίστας.", - "LISTS_SET_INDEX_TOOLTIP_INSERT_LAST": "Αναθέτει το στοιχείο στο τέλος μιας λίστας.", + "LISTS_SET_INDEX_TOOLTIP_INSERT_LAST": "Πρόσθεσε το στοιχείο στο τέλος μιας λίστας.", "LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM": "Εισάγει το στοιχείο τυχαία σε μια λίστα.", "LISTS_GET_SUBLIST_START_FROM_START": "πάρε υπολίστα από #", "LISTS_GET_SUBLIST_START_FROM_END": "πάρε υπολίστα από # από το τέλος", @@ -306,50 +307,50 @@ "LISTS_GET_SUBLIST_END_FROM_END": "έως # από το τέλος", "LISTS_GET_SUBLIST_END_LAST": "έως το τελευταίο", "LISTS_GET_SUBLIST_TOOLTIP": "Δημιουργεί ένα αντίγραφο του καθορισμένου τμήματος μιας λίστας.", - "LISTS_SORT_TITLE": "επιλογή %1 %2 %3", - "LISTS_SORT_TOOLTIP": "Επιλέξετε ένα αντίγραφο της λίστας.", - "LISTS_SORT_ORDER_ASCENDING": "Αύξουσα", - "LISTS_SORT_ORDER_DESCENDING": "Φθίνουσα", - "LISTS_SORT_TYPE_NUMERIC": "αριθμητικό", + "LISTS_SORT_TITLE": "ταξινόμησε %1 %2 %3", + "LISTS_SORT_TOOLTIP": "Ταξινομήστε ένα αντίγραφο μιας λίστας.", + "LISTS_SORT_ORDER_ASCENDING": "αύξουσα", + "LISTS_SORT_ORDER_DESCENDING": "φθίνουσα", + "LISTS_SORT_TYPE_NUMERIC": "αριθμητικά", "LISTS_SORT_TYPE_TEXT": "Αλφαβητικά", - "LISTS_SORT_TYPE_IGNORECASE": "αλφαβητικά, αγνοήστε το στοιχείο", + "LISTS_SORT_TYPE_IGNORECASE": "αλφαβητικά, αγνοώντας πεζά/κεφαλαία γράμματα", "LISTS_SPLIT_LIST_FROM_TEXT": "κάνετε λίστα από το κείμενο", - "LISTS_SPLIT_TEXT_FROM_LIST": "κάνετε κείμενο από τη λίστα", + "LISTS_SPLIT_TEXT_FROM_LIST": "δημιούργησε κείμενο από τη λίστα", "LISTS_SPLIT_WITH_DELIMITER": "με διαχωριστικό", - "LISTS_SPLIT_TOOLTIP_SPLIT": "Διαίρεση του κειμένου σε μια λίστα κειμένων, με σπάσιμο σε κάθε διαχωριστικό.", + "LISTS_SPLIT_TOOLTIP_SPLIT": "Διαχώρισε το κείμενο σε μια λίστα κειμένων, σπάζοντας το σε κάθε διαχωριστικό.", "LISTS_SPLIT_TOOLTIP_JOIN": "Ενώστε μια λίστα κειμένων σε ένα κείμενο, που χωρίζονται από ένα διαχωριστικό.", - "LISTS_REVERSE_MESSAGE0": "ανάκληση %1", - "LISTS_REVERSE_TOOLTIP": "Α", + "LISTS_REVERSE_MESSAGE0": "αντιστροφή %1", + "LISTS_REVERSE_TOOLTIP": "Αντιστρέφει ένα αντίγραφο της λίστας.", "VARIABLES_GET_TOOLTIP": "Επιστρέφει την τιμή αυτής της μεταβλητής.", - "VARIABLES_GET_CREATE_SET": "Δημιούργησε «όρισε %1»", - "VARIABLES_SET": "ανάθεσε στην %1 το %2", + "VARIABLES_GET_CREATE_SET": "Δημιούργησε 'όρισε %1'", + "VARIABLES_SET": "όρισε το %1 σε %2", "VARIABLES_SET_TOOLTIP": "Ορίζει αυτή τη μεταβλητή να είναι ίση με την είσοδο.", - "VARIABLES_SET_CREATE_GET": "Δημιούργησε «πάρε %1»", - "PROCEDURES_DEFNORETURN_TITLE": "στο", + "VARIABLES_SET_CREATE_GET": "Δημιούργησε 'πάρε %1'", + "PROCEDURES_DEFNORETURN_TITLE": "σε", "PROCEDURES_DEFNORETURN_PROCEDURE": "κάνε κάτι", "PROCEDURES_BEFORE_PARAMS": "με:", "PROCEDURES_CALL_BEFORE_PARAMS": "με:", "PROCEDURES_DEFNORETURN_TOOLTIP": "Δημιουργεί μια συνάρτηση χωρίς έξοδο.", - "PROCEDURES_DEFNORETURN_COMMENT": "Περιγράψετε αυτήν την ιδιότητα..", + "PROCEDURES_DEFNORETURN_COMMENT": "Περίγραψε αυτή την συνάρτηση..", "PROCEDURES_DEFRETURN_RETURN": "επέστρεψε", "PROCEDURES_DEFRETURN_TOOLTIP": "Δημιουργεί μια συνάρτηση με μια έξοδο.", - "PROCEDURES_ALLOW_STATEMENTS": "να επιτρέπονται οι δηλώσεις", + "PROCEDURES_ALLOW_STATEMENTS": "επέτρεψε δηλώσεις", "PROCEDURES_DEF_DUPLICATE_WARNING": "Προειδοποίηση: Αυτή η συνάρτηση έχει διπλότυπες παραμέτρους.", "PROCEDURES_CALLNORETURN_HELPURL": "https://el.wikipedia.org/wiki/%CE%94%CE%B9%CE%B1%CE%B4%CE%B9%CE%BA%CE%B1%CF%83%CE%AF%CE%B1_%28%CF%85%CF%80%CE%BF%CE%BB%CE%BF%CE%B3%CE%B9%CF%83%CF%84%CE%AD%CF%82%29", - "PROCEDURES_CALLNORETURN_TOOLTIP": "Εκτελεί την ορισμένη από τον χρήστη συνάρτηση «%1».", + "PROCEDURES_CALLNORETURN_TOOLTIP": "Εκτέλεσε την ορισμένη από τον χρήστη συνάρτηση '%1'.", "PROCEDURES_CALLRETURN_HELPURL": "https://el.wikipedia.org/wiki/%CE%94%CE%B9%CE%B1%CE%B4%CE%B9%CE%BA%CE%B1%CF%83%CE%AF%CE%B1_%28%CF%85%CF%80%CE%BF%CE%BB%CE%BF%CE%B3%CE%B9%CF%83%CF%84%CE%AD%CF%82%29", - "PROCEDURES_CALLRETURN_TOOLTIP": "Εκτελεί την ορισμένη από τον χρήστη συνάρτηση «%1» και χρησιμοποίησε την έξοδό της.", + "PROCEDURES_CALLRETURN_TOOLTIP": "Εκτέλεσε την ορισμένη από τον χρήστη συνάρτηση '%1' και χρησιμοποίησε την έξοδό της.", "PROCEDURES_MUTATORCONTAINER_TITLE": "είσοδοι", - "PROCEDURES_MUTATORCONTAINER_TOOLTIP": "Προσθέτει, αφαιρεί ή αναδιατάσσει εισόδους σε αυτήν τη λειτουργία", + "PROCEDURES_MUTATORCONTAINER_TOOLTIP": "Πρόσθεσε, αφαίρεσε ή αναδιάταξε τις εισόδους σε αυτήν τη συνάρτηση.", "PROCEDURES_MUTATORARG_TITLE": "όνομα εισόδου:", - "PROCEDURES_MUTATORARG_TOOLTIP": "Πρόσθεσε μια είσοδος στη συνάρτηση", + "PROCEDURES_MUTATORARG_TOOLTIP": "Πρόσθεσε μια είσοδο στη συνάρτηση", "PROCEDURES_HIGHLIGHT_DEF": "Επισημάνετε τον ορισμό συνάρτησης", - "PROCEDURES_CREATE_DO": "Δημιούργησε «%1»", + "PROCEDURES_CREATE_DO": "Δημιούργησε '%1'", "PROCEDURES_IFRETURN_TOOLTIP": "Αν μια τιμή είναι αληθής, τότε επιστρέφει τη δεύτερη τιμή.", "PROCEDURES_IFRETURN_WARNING": "Προειδοποίηση: Αυτό το μπλοκ μπορεί να χρησιμοποιηθεί μόνο στον ορισμό μιας συνάρτησης.", - "WORKSPACE_COMMENT_DEFAULT_TEXT": "Πείτε κάτι...", + "WORKSPACE_COMMENT_DEFAULT_TEXT": "Πες κάτι...", "WORKSPACE_ARIA_LABEL": "Χώρος εργασίας Blockly", - "COLLAPSED_WARNINGS_WARNING": "Υπάρχουν προειδοποιήσεις για συμπτυγμένα μπλοκ.", + "COLLAPSED_WARNINGS_WARNING": "Υπάρχουν προειδοποιήσεις στα συμπτυγμένα μπλοκ.", "DIALOG_OK": "Εντάξει", "DIALOG_CANCEL": "Ακύρωση" } diff --git a/msg/json/en.json b/msg/json/en.json index d9bddec31e9..50800bc27e8 100644 --- a/msg/json/en.json +++ b/msg/json/en.json @@ -1,7 +1,7 @@ { "@metadata": { "author": "Ellen Spertus ", - "lastupdated": "2023-12-08 18:42:04.679586", + "lastupdated": "2024-04-16 23:19:53.668551", "locale": "en", "messagedocumentation" : "qqq" }, @@ -291,10 +291,11 @@ "LISTS_ISEMPTY_TITLE": "%1 is empty", "LISTS_ISEMPTY_TOOLTIP": "Returns true if the list is empty.", "LISTS_INLIST": "in list", - "LISTS_INDEX_OF_HELPURL": "https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list", + "LISTS_INDEX_OF_HELPURL": "https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list", "LISTS_INDEX_OF_FIRST": "find first occurrence of item", "LISTS_INDEX_OF_LAST": "find last occurrence of item", "LISTS_INDEX_OF_TOOLTIP": "Returns the index of the first/last occurrence of the item in the list. Returns %1 if item is not found.", + "LISTS_GET_INDEX_HELPURL": "https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list", "LISTS_GET_INDEX_GET": "get", "LISTS_GET_INDEX_GET_REMOVE": "get and remove", "LISTS_GET_INDEX_REMOVE": "remove", @@ -369,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/es.json b/msg/json/es.json index 12e39c14adb..66118d46dad 100644 --- a/msg/json/es.json +++ b/msg/json/es.json @@ -4,6 +4,7 @@ "Ajeje Brazorf", "Atzerritik", "Codynguyen1116", + "Eulalio", "Fitoschido", "Harvest", "Indiralena", @@ -267,6 +268,7 @@ "LISTS_INDEX_OF_FIRST": "encontrar la primera aparición del elemento", "LISTS_INDEX_OF_LAST": "encontrar la última aparición del elemento", "LISTS_INDEX_OF_TOOLTIP": "Devuelve el índice de la primera/última aparición del elemento en la lista. Devuelve %1 si el elemento no se encuentra.", + "LISTS_GET_INDEX_HELPURL": "https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list", "LISTS_GET_INDEX_GET": "obtener", "LISTS_GET_INDEX_GET_REMOVE": "obtener y eliminar", "LISTS_GET_INDEX_REMOVE": "eliminar", diff --git a/msg/json/fa.json b/msg/json/fa.json index 6e8fac8cb5a..a27d412ab45 100644 --- a/msg/json/fa.json +++ b/msg/json/fa.json @@ -299,8 +299,8 @@ "LISTS_SORT_ORDER_ASCENDING": "صعودی", "LISTS_SORT_ORDER_DESCENDING": "نزولی", "LISTS_SORT_TYPE_NUMERIC": "عددی", - "LISTS_SORT_TYPE_TEXT": "حروفی ، الفبایی", - "LISTS_SORT_TYPE_IGNORECASE": "حروفی ، رد کردن مورد", + "LISTS_SORT_TYPE_TEXT": "حروفی، الفبایی", + "LISTS_SORT_TYPE_IGNORECASE": "حروفی، در نظر نگرفتن حروف بزرگ و کوچک", "LISTS_SPLIT_LIST_FROM_TEXT": "ایجاد فهرست از متن", "LISTS_SPLIT_TEXT_FROM_LIST": "ایجاد متن از فهرست", "LISTS_SPLIT_WITH_DELIMITER": "همراه جداساز", diff --git a/msg/json/fr.json b/msg/json/fr.json index 1f41860d355..4d6b9894ca6 100644 --- a/msg/json/fr.json +++ b/msg/json/fr.json @@ -3,6 +3,7 @@ "authors": [ "Alacabe", "Appr", + "Ashleyzeldin", "Chrisbansart", "Espertus", "Fredlefred", @@ -24,7 +25,7 @@ }, "VARIABLES_DEFAULT_NAME": "élément", "UNNAMED_KEY": "non nommé", - "TODAY": "Aujourd'hui", + "TODAY": "Aujourd’hui", "DUPLICATE_BLOCK": "Dupliquer", "ADD_COMMENT": "Ajouter un commentaire", "REMOVE_COMMENT": "Supprimer un commentaire", @@ -55,7 +56,7 @@ "NEW_VARIABLE_TITLE": "Nom de la nouvelle variable :", "VARIABLE_ALREADY_EXISTS": "Une variable nommée « %1 » existe déjà.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Une variable nommée « %1 » existe déjà pour un autre type : « %2 ».", - "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Une variable nommée '%1' existe déjà comme paramètre dans la procédure '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Une variable nommée « %1 » existe déjà comme paramètre dans la procédure « %2 ».", "DELETE_VARIABLE_CONFIRMATION": "Supprimer %1 utilisations de la variable « %2 » ?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Impossible de supprimer la variable « %1 » parce qu’elle fait partie de la définition de la fonction « %2 »", "DELETE_VARIABLE": "Supprimer la variable « %1 »", @@ -111,7 +112,7 @@ "LOGIC_COMPARE_TOOLTIP_LT": "Renvoyer vrai si la première entrée est plus petite que la seconde.", "LOGIC_COMPARE_TOOLTIP_LTE": "Renvoyer vrai si la première entrée est plus petite ou égale à la seconde.", "LOGIC_COMPARE_TOOLTIP_GT": "Renvoyer vrai si la première entrée est plus grande que la seconde.", - "LOGIC_COMPARE_TOOLTIP_GTE": "Renvoyer true si la première entrée est supérieure ou égale à la seconde.", + "LOGIC_COMPARE_TOOLTIP_GTE": "Renvoyer vrai si la première entrée est supérieure ou égale à la seconde.", "LOGIC_OPERATION_HELPURL": "https://fr.wikipedia.org/wiki/Connecteur_logique", "LOGIC_OPERATION_TOOLTIP_AND": "Renvoyer vrai si les deux entrées sont vraies.", "LOGIC_OPERATION_AND": "et", diff --git a/msg/json/gl.json b/msg/json/gl.json index e2d5805def0..62f4192c3ea 100644 --- a/msg/json/gl.json +++ b/msg/json/gl.json @@ -249,6 +249,7 @@ "LISTS_INDEX_OF_FIRST": "atopar a primeira aparición do elemento", "LISTS_INDEX_OF_LAST": "atopar a derradeira aparición do elemento", "LISTS_INDEX_OF_TOOLTIP": "Devolve o índice da primeira/derradeira aparición do primeiro elemento na lista. Devolve %1 se non se atopa o elemento.", + "LISTS_GET_INDEX_HELPURL": "https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list", "LISTS_GET_INDEX_GET": "obter", "LISTS_GET_INDEX_GET_REMOVE": "obter e eliminar", "LISTS_GET_INDEX_REMOVE": "eliminar", @@ -312,6 +313,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "facer algo", "PROCEDURES_BEFORE_PARAMS": "con:", "PROCEDURES_CALL_BEFORE_PARAMS": "con:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Non se pode executar a función \"%1\" definida polo usuario porque o bloque de definición está desactivado.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Crea unha función sen devolución.", "PROCEDURES_DEFNORETURN_COMMENT": "Describe esta función...", "PROCEDURES_DEFRETURN_RETURN": "devolver", diff --git a/msg/json/ia.json b/msg/json/ia.json index 42cd70c9b22..f7600660271 100644 --- a/msg/json/ia.json +++ b/msg/json/ia.json @@ -248,6 +248,7 @@ "LISTS_INDEX_OF_FIRST": "cercar le prime occurrentia del elemento", "LISTS_INDEX_OF_LAST": "cercar le ultime occurrentia del elemento", "LISTS_INDEX_OF_TOOLTIP": "Retorna le indice del prime/ultime occurrentia del elemento in le lista. Retorna %1 si le elemento non es trovate.", + "LISTS_GET_INDEX_HELPURL": "https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list", "LISTS_GET_INDEX_GET": "prender", "LISTS_GET_INDEX_GET_REMOVE": "prender e remover", "LISTS_GET_INDEX_REMOVE": "remover", @@ -311,6 +312,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "facer qualcosa", "PROCEDURES_BEFORE_PARAMS": "con:", "PROCEDURES_CALL_BEFORE_PARAMS": "con:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Non es possibile executar le function definite per le usator ‘%1’ perque le bloco de definition es disactivate.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Crea un function que non retorna un valor.", "PROCEDURES_DEFNORETURN_COMMENT": "Describe iste function...", "PROCEDURES_DEFRETURN_RETURN": "retornar", diff --git a/msg/json/ko.json b/msg/json/ko.json index 85be433f522..bbfa5eb9488 100644 --- a/msg/json/ko.json +++ b/msg/json/ko.json @@ -5,6 +5,7 @@ "Amire80", "Codenstory", "Delanoor", + "Dr1t jg", "Gongsoonyee", "Hym411", "JeonHK", @@ -19,6 +20,7 @@ "Revi", "SeoJeongHo", "Snddl3", + "Suleiman the Magnificent Television", "Ykhwong", "아라" ] @@ -56,6 +58,7 @@ "NEW_VARIABLE_TITLE": "새 변수 이름:", "VARIABLE_ALREADY_EXISTS": "'%1' 변수는 이미 존재합니다.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "'%1' 변수는 다른 유형에 대해 이미 존재합니다: '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "'%1' 변수가 이미 '%2' 프로시저의 변수로 존재합니다.", "DELETE_VARIABLE_CONFIRMATION": "'%2' 변수에서 %1을(를) 삭제하시겠습니까?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "'%2' 함수 정의의 일부이기 때문에 '%1' 변수를 삭제할 수 없습니다", "DELETE_VARIABLE": "'%1' 변수를 삭제합니다", @@ -341,6 +344,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "함수 이름", "PROCEDURES_BEFORE_PARAMS": "사용:", "PROCEDURES_CALL_BEFORE_PARAMS": "사용:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "정의 블록이 비활성화되었기 때문에 사용자 정의 함수 '%1'를 실행할 수 없습니다.", "PROCEDURES_DEFNORETURN_TOOLTIP": "실행 후, 결과 값을 돌려주지 않는 함수를 만듭니다.", "PROCEDURES_DEFNORETURN_COMMENT": "이 함수를 설명하세요...", "PROCEDURES_DEFRETURN_HELPURL": "https://ko.wikipedia.org/wiki/함수_(컴퓨터_과학)", diff --git a/msg/json/ms.json b/msg/json/ms.json index 50bf66c8df0..1ed6c50d63b 100644 --- a/msg/json/ms.json +++ b/msg/json/ms.json @@ -2,7 +2,8 @@ "@metadata": { "authors": [ "Anakmalaysia", - "Espertus" + "Espertus", + "Hakimi97" ] }, "VARIABLES_DEFAULT_NAME": "Perkara", @@ -282,7 +283,7 @@ "PROCEDURES_DEFNORETURN_COMMENT": "Terangkan fungsi ini...", "PROCEDURES_DEFRETURN_RETURN": "kembali", "PROCEDURES_DEFRETURN_TOOLTIP": "Mencipta satu fungsi dengan pengeluaran.", - "PROCEDURES_ALLOW_STATEMENTS": "bolehkan kenyataan", + "PROCEDURES_ALLOW_STATEMENTS": "membenarkan kenyataan", "PROCEDURES_DEF_DUPLICATE_WARNING": "Amaran: Fungsi ini mempunyai parameter yang berganda.", "PROCEDURES_CALLNORETURN_TOOLTIP": "Run the user-defined function '%1'.", "PROCEDURES_CALLRETURN_TOOLTIP": "Run the user-defined function '%1' and use its output.", diff --git a/msg/json/ne.json b/msg/json/ne.json index f4af6bab78a..3a4c1c0de76 100644 --- a/msg/json/ne.json +++ b/msg/json/ne.json @@ -2,47 +2,246 @@ "@metadata": { "authors": [ "Bada Kaji", + "Yadav Bhattarai", "बडा काजी", "सरोज कुमार ढकाल" ] }, "VARIABLES_DEFAULT_NAME": "वस्तु", + "UNNAMED_KEY": "अज्ञात", "TODAY": "आज", - "DUPLICATE_BLOCK": "प्रतिलिपी गर्ने", + "DUPLICATE_BLOCK": "प्रतिलिपी", "ADD_COMMENT": "टिप्पणी थप्ने", "REMOVE_COMMENT": "टिप्पणी हटाउने", + "DUPLICATE_COMMENT": "समिक्षाको प्रतिलिपीकरण", "EXTERNAL_INPUTS": "बाह्य इन्पुटहरू", - "INLINE_INPUTS": "इनलाइन इन्पुटहरू", + "INLINE_INPUTS": "यहीका इन्पुटहरू", "DELETE_BLOCK": "ब्लक हटाउने", "DELETE_X_BLOCKS": "%1 ब्लकहरू हटाउने", - "CLEAN_UP": "ब्लकहरू हटाउनुहोस्", + "DELETE_ALL_BLOCKS": "सबै %1 ब्लकहरू मेट्ने हो?", + "CLEAN_UP": "ब्लकहरू सफा गर्नुहोस्", "COLLAPSE_BLOCK": "ब्लक भत्काउनुहोस्", - "COLLAPSE_ALL": "ब्लक भत्काउनुहोस्", + "COLLAPSE_ALL": "ब्लकहरू भत्काउनुहोस्", "EXPAND_BLOCK": "ब्लकहरू फिजाउने", "EXPAND_ALL": "ब्लकहरू फिजाउने", - "DISABLE_BLOCK": "ब्लकलाई सक्रिय पार्ने", - "ENABLE_BLOCK": "ब्लकलाई निश्कृयपार्ने", + "DISABLE_BLOCK": "ब्लकलाई निस्कृय पार्ने", + "ENABLE_BLOCK": "ब्लकलाई सकृय पार्ने", "HELP": "सहायता", "UNDO": "रद्द गर्ने", - "REDO": "पुन: कायम गर्ने", - "CHANGE_VALUE_TITLE": "मान बदल्नुहोस :", - "RENAME_VARIABLE": "भेरिएबल पुन: नामाकरण गर्ने ...", - "NEW_VARIABLE": "नयाँ भेरिएबल ...", - "NEW_VARIABLE_TITLE": "नयाँ भेरिएबल नाम:", - "COLOUR_RANDOM_TITLE": "जुनसुकै रङ्ग", - "COLOUR_RGB_TITLE": "यस रङ्गको", + "REDO": "फेरी गर्ने", + "CHANGE_VALUE_TITLE": "मान बदल्नुहोस:", + "RENAME_VARIABLE": "चरको पुन: नामाकरण गर्ने ...", + "RENAME_VARIABLE_TITLE": "सबै '%1' चरहरूको नाम परिवर्तन गर्नुहोस्:", + "NEW_VARIABLE": "चर बनाउ...", + "NEW_STRING_VARIABLE": "स्ट्रिङ चर बनाउनुहोस्...", + "NEW_NUMBER_VARIABLE": "अंक चर बनाउनुहोस्...", + "NEW_COLOUR_VARIABLE": "रङ्ग चर बनाउनुहोस्...", + "NEW_VARIABLE_TYPE_TITLE": "नयाँ चरको प्रकार:", + "NEW_VARIABLE_TITLE": "नयाँ चरको नाम:", + "VARIABLE_ALREADY_EXISTS": "'%1' नाम भएको चर पहिल्यै छ।", + "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "'%1' नाम भएको चर अर्को प्रकार:'%2' कोलागी पहिल्यै छ।", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "'%1' नामको चल पहिले नै '%2' प्रक्रियामा प्यारामिटर हो।", + "DELETE_VARIABLE_CONFIRMATION": "'%2' चलको %1 प्रयोगहरू हटाउने हो?", + "CANNOT_DELETE_VARIABLE_PROCEDURE": "चर '%1' मेटाउन सकिँदैन किनभने यो प्रकार्य '%2' को परिभाषाको अंश हो।", + "DELETE_VARIABLE": "'%1' चर हटाउनुहोस्", + "COLOUR_PICKER_TOOLTIP": "रङ् थालीबाट छान्नुहोस्।", + "COLOUR_RANDOM_TITLE": "कुनै रङ्ग", + "COLOUR_RANDOM_TOOLTIP": "अनियमित रूपमा रङ छान्नुहोस्।", + "COLOUR_RGB_TITLE": "रङ्गाउ", "COLOUR_RGB_RED": "रातो", "COLOUR_RGB_GREEN": "हरियो", "COLOUR_RGB_BLUE": "निलो", + "COLOUR_RGB_TOOLTIP": "रातो, हरियो र नीलो को निर्दिष्ट मात्रा लिएर एउटा रङ बनाउनुहोस्। सबै मात्राहरू ० देखी १०० को बीचको हुनुपर्छ।", + "COLOUR_BLEND_TITLE": "मिश्रण", "COLOUR_BLEND_COLOUR1": "रङ्ग १", "COLOUR_BLEND_COLOUR2": "रङ्ग २", "COLOUR_BLEND_RATIO": "अनुपात", - "MATH_IS_EVEN": "जोर संख्या हो", - "MATH_IS_ODD": "बिजोर संख्या हो", - "MATH_IS_PRIME": "रुढ संख्या हो", - "MATH_IS_POSITIVE": "धनात्मक संख्या हो", - "MATH_IS_NEGATIVE": "ऋणात्मक संख्या हो", + "COLOUR_BLEND_TOOLTIP": "दिइएको अनुपात(0.0 - 1.0)मा दुई रङहरू मिलाउँछ।", + "CONTROLS_REPEAT_TITLE": "%1 पटक दोहोऱ्याउनुहोस्", + "CONTROLS_REPEAT_INPUT_DO": "गर्नुहोस्", + "CONTROLS_REPEAT_TOOLTIP": "केही कथनहरू केही पटक दोहोऱ्याउनुहोस्।", + "CONTROLS_WHILEUNTIL_OPERATOR_WHILE": "भएसम्म दोहोऱ्याउनुहोस्", + "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "यस सम्म दोहोर्‌याउने", + "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "मान साँचो भईन्जेल केहि कथनहरू दोहोऱ्याउनुहोस्।", + "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "मान असत्य भईन्जेल केहि कथनहरू दोहोऱ्याउनुहोस्।", + "CONTROLS_FOR_TOOLTIP": "चर '%1' लाई सुरु देखि अन्त्य सङ्ख्यासम्मका मानहरू लिन दिनुहोस्, निर्दिष्ट अन्तरालमा गन्दै निर्दिष्ट ब्लकहरू दोहोऱ्याउनुहोस्।", + "CONTROLS_FOR_TITLE": "%1लाई %2 देखी %3 सम्म %4 जोड्दै गन्नुहोस्।", + "CONTROLS_FOREACH_TITLE": "सूची %2 का प्रत्येक वस्तु %1 लाई", + "CONTROLS_FOREACH_TOOLTIP": "सूचीका बस्तुहरूमा एक पटकमा एक वस्तु चल '%1' मा राखेर केही कथनहरू गर्नुहोस्।", + "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "नदोहोऱ्याउनुहोस्", + "CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE": "अर्को चरणमा जानुहोस्", + "CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK": "आन्तरिक दोहोरान सकियो।", + "CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE": "बाँकी छाडेर अर्को चरणमा सुरू गर्नुहोस्।", + "CONTROLS_FLOW_STATEMENTS_WARNING": "सावधान: यो ब्लक बृत्त भित्रमात्र प्रयोग गरिन्छ।", + "CONTROLS_IF_TOOLTIP_1": "यदि मान साँचो भए केही कथनहरू गर्नुहोस्।", + "CONTROLS_IF_TOOLTIP_2": "यदि मान सत्य भए बयानको पहिलो ब्लक गर्नुहोस् नत्र दोस्रो।", + "CONTROLS_IF_TOOLTIP_3": "यदि पहिलो मान सत्य भए बयानको पहिलो ब्लक नत्र यदि दोस्रो मान सत्य भए दोस्रो ब्लक गर्नुहोस्।", + "CONTROLS_IF_TOOLTIP_4": "यदि पहिलो मान सत्य भए कथनको पहिलो ब्लक नत्र यदि दोस्रो मान सत्य भए दोस्रो ब्लक नत्र ती दुबै सत्य नभए अन्तिम ब्लक गर्नुहोस्।", + "CONTROLS_IF_MSG_IF": "if", + "CONTROLS_IF_MSG_ELSEIF": "else if", + "CONTROLS_IF_MSG_ELSE": "else", + "CONTROLS_IF_IF_TOOLTIP": "यो if भनाईलाई मिलाउन खण्डहरू थप्ने, हटाउने वा मिलाउने गर्नुहोस्।", + "CONTROLS_IF_ELSEIF_TOOLTIP": "यो if भनाईमा एउटा प्रश्न थप्नुहोस्।", + "CONTROLS_IF_ELSE_TOOLTIP": "यो if भनाईमा एउटा final, catch-all सर्त थप्नुहोस्।", + "LOGIC_COMPARE_TOOLTIP_EQ": "यदि दुबै दिइएका कुराहरू बराबर भए true फर्काउनुहोस।", + "LOGIC_COMPARE_TOOLTIP_NEQ": "यदि दुबै दिइएका कुराहरू आफुमा बराबर नभए true फर्काउनुहोस।", + "LOGIC_COMPARE_TOOLTIP_LT": "यदि दिइएका पहिलो, दोश्रो भन्दा सानो भए true फर्काउनुहोस।", + "LOGIC_COMPARE_TOOLTIP_LTE": "यदि दिइएका पहिलो, दोश्रो भन्दा सानो वा बराबर भए true फर्काउनुहोस।", + "LOGIC_COMPARE_TOOLTIP_GT": "यदि दिइएका पहिलो, दोश्रो भन्दा ठुलो भए true फर्काउनुहोस।", + "LOGIC_COMPARE_TOOLTIP_GTE": "यदि दिइएका पहिलो, दोश्रो भन्दा ठुलो वा बराबर भए true फर्काउनुहोस।", + "LOGIC_OPERATION_TOOLTIP_AND": "यदि दिइएका दुबै सत्य भए true फर्काउनुहोस्।", + "LOGIC_OPERATION_AND": "and", + "LOGIC_OPERATION_TOOLTIP_OR": "यदि कम्तिमा एउटा दिइएको कुरा साँचो छ भने true फर्काउनुहोस्।", + "LOGIC_OPERATION_OR": "or", + "LOGIC_NEGATE_TITLE": "not %1", + "LOGIC_NEGATE_TOOLTIP": "यदि दिइएको गलत भए true फर्काउँछ। यदि दिइएको सही छ भने false फर्काउँछ।", + "LOGIC_BOOLEAN_TRUE": "true", + "LOGIC_BOOLEAN_FALSE": "false", + "LOGIC_BOOLEAN_TOOLTIP": "true वा false फर्काउँछ।", + "LOGIC_NULL": "null", + "LOGIC_NULL_TOOLTIP": "null फर्काउछ।", + "LOGIC_TERNARY_CONDITION": "परीक्षण", + "LOGIC_TERNARY_IF_TRUE": "सत्य भए", + "LOGIC_TERNARY_IF_FALSE": "गलत भए", + "LOGIC_TERNARY_TOOLTIP": "'परीक्षण' मा अवस्था जाँच गर्नुहोस्। यदि सर्त सत्य भए, 'if true' मान फर्काउँछ; अन्यथा 'if false'।", + "MATH_NUMBER_TOOLTIP": "एउटा सङ्ख्या।", + "MATH_TRIG_SIN": "sin", + "MATH_TRIG_COS": "cos", + "MATH_TRIG_TAN": "tan", + "MATH_TRIG_ASIN": "asin", + "MATH_TRIG_ACOS": "acos", + "MATH_TRIG_ATAN": "atan", + "MATH_ARITHMETIC_TOOLTIP_ADD": "दुई सङ्ख्याको योग फर्काउनुहोस्।", + "MATH_ARITHMETIC_TOOLTIP_MINUS": "दुई संख्याको भिन्नता फर्काउनुहोस्।", + "MATH_ARITHMETIC_TOOLTIP_MULTIPLY": "दुई सङ्ख्याको गुणन फर्काउनुहोस्।", + "MATH_ARITHMETIC_TOOLTIP_DIVIDE": "दुई संख्याको भागफल फर्काउनुहोस्।", + "MATH_ARITHMETIC_TOOLTIP_POWER": "पहिलो सङ्ख्यालाई दोस्रो सङ्ख्या पटक गुणा गरेर आएको सङ्ख्या फर्काउनुहोस्।", + "MATH_SINGLE_OP_ROOT": "वर्गमूल", + "MATH_SINGLE_TOOLTIP_ROOT": "संख्याको वर्गमूल फर्काउनुहोस्।", + "MATH_SINGLE_OP_ABSOLUTE": "निरपेक्ष", + "MATH_SINGLE_TOOLTIP_ABS": "एउटा संख्याको निरपेक्ष मान फर्काउनुहोस्।", + "MATH_SINGLE_TOOLTIP_NEG": "संख्याको नकारात्मक मान फर्काउनुहोस्।", + "MATH_SINGLE_TOOLTIP_LN": "एउटा सङ्ख्याको प्राकृतिक लघुगणक फर्काउनुहोस्।", + "MATH_SINGLE_TOOLTIP_LOG10": "एउटा सङ्ख्याको आधार १० को लघुगणक फर्काउनुहोस्।", + "MATH_SINGLE_TOOLTIP_EXP": "संख्याको e को पावर फर्काउनुहोस्।", + "MATH_SINGLE_TOOLTIP_POW10": "सङ्ख्याको 10 को पावर फर्काउनुहोस्।", + "MATH_TRIG_TOOLTIP_SIN": "एउटा कोणको sine फर्काउनुहोस् (रेडियन होईन)।", + "MATH_TRIG_TOOLTIP_COS": "एउटा कोणको cosine फर्काउनुहोस् (रेडियन होईन)।", + "MATH_TRIG_TOOLTIP_TAN": "एउटा कोणको tangent फर्काउनुहोस् (रेडियन होईन)।", + "MATH_TRIG_TOOLTIP_ASIN": "एउटा कोणको arcsine फर्काउनुहोस्।", + "MATH_TRIG_TOOLTIP_ACOS": "एउटा कोणको arccosine फर्काउनुहोस्।", + "MATH_TRIG_TOOLTIP_ATAN": "एउटा कोणको arctangent फर्काउनुहोस्।", + "MATH_CONSTANT_TOOLTIP": "सामान्य स्थिरांक मध्ये एउटा फर्काउनुहोस्: π (3.141…), e (2.718…), φ (1.618…), sqrt(2) (1.414…), sqrt(½) (0.707…), वा ∞ (अनन्तता)।", + "MATH_IS_EVEN": "जोर", + "MATH_IS_ODD": "बिजोर", + "MATH_IS_PRIME": "अविभाज्य", + "MATH_IS_WHOLE": "पूर्ण छ", + "MATH_IS_POSITIVE": "धनात्मक", + "MATH_IS_NEGATIVE": "ऋणात्मक", + "MATH_IS_DIVISIBLE_BY": "द्वारा विभाजित छ", + "MATH_IS_TOOLTIP": "जाँच गर्नुहोस् कि संख्या एक सम, बिजोड, अभाज्य, पूर्ण, धनात्मक, ऋणात्मक, वा यदि यो निश्चित संख्या द्वारा विभाजित छ। सही वा गलत फर्काउँछ।", + "MATH_CHANGE_TITLE": "%1 लाई %2 ले परिवर्तन गर्नुहोस्", + "MATH_CHANGE_TOOLTIP": "चल '%1' मा संख्या थप्नुहोस्।", + "MATH_ROUND_TOOLTIP": "सङ्ख्यालाई ठुलो वा सानो पूर्ण सङ्ख्या बनाउनुहोस्।", + "MATH_ROUND_OPERATOR_ROUND": "पूर्ण सङ्ख्या", + "MATH_ROUND_OPERATOR_ROUNDUP": "ठुलो पूर्ण सङ्ख्या", + "MATH_ROUND_OPERATOR_ROUNDDOWN": "सानो पूर्ण सङ्ख्या", + "MATH_ONLIST_OPERATOR_SUM": "सूचीको योगफल", + "MATH_ONLIST_TOOLTIP_SUM": "सूचीमा भएका सबै सङ्ख्याहरूको योगफल फर्काउनुहोस्।", + "MATH_ONLIST_OPERATOR_MIN": "सूचीको न्यूनतम", + "MATH_ONLIST_TOOLTIP_MIN": "सूचीमा सबैभन्दा सानो सङ्ख्या फर्काउनुहोस्।", + "MATH_ONLIST_OPERATOR_MAX": "सूचीको अधिकतम", + "MATH_ONLIST_TOOLTIP_MAX": "सूचीमा सबैभन्दा ठूलो सङ्ख्या फर्काउनुहोस्।", + "MATH_ONLIST_OPERATOR_AVERAGE": "सूचीको औसत", + "MATH_ONLIST_TOOLTIP_AVERAGE": "सूचीमा संख्यात्मक मानहरूको औसत (अंकगणितीय माध्य) फिर्ता गर्नुहोस्।", + "MATH_ONLIST_OPERATOR_MEDIAN": "सूचीको मध्यमा", + "MATH_ONLIST_TOOLTIP_MEDIAN": "सूचीमा मध्य संख्या फर्काउनुहोस्।", + "MATH_ONLIST_TOOLTIP_MODE": "सूचीबाट सबैभन्दा सामान्य वस्तु(हरू) को सूची फर्काउनुहोस्।", + "MATH_ONLIST_OPERATOR_STD_DEV": "सूचीको मानक विचलन", + "MATH_ONLIST_TOOLTIP_STD_DEV": "सूचीको मानक विचलन फर्काउनुहोस्।", + "MATH_ONLIST_OPERATOR_RANDOM": "सूचीको कुनै वस्तु", + "MATH_ONLIST_TOOLTIP_RANDOM": "सूचीबाट कुनै एक फर्काउनुहोस्।", + "MATH_MODULO_TITLE": "%1 ÷ %2 को बाँकी", + "MATH_MODULO_TOOLTIP": "दुई संख्याहरूको भाग गरेर बाँकी फर्काउनुहोस्।", + "MATH_CONSTRAIN_TITLE": "सीमित %1 सानो %2 ठुलो %3", + "MATH_CONSTRAIN_TOOLTIP": "एउटा सङ्ख्यालाई निर्दिष्ट सीमाहरूमा (समावेशी) सीमित गर्नुहोस्।", + "MATH_RANDOM_INT_TITLE": "%1 देखि %2 बिचको कुनै पूर्ण सङ्खया", + "MATH_RANDOM_INT_TOOLTIP": "दुई निर्दिष्ट समावेशी सीमाहरू बीचको कुनै पूर्णांक फर्काउनुहोस्।", + "MATH_RANDOM_FLOAT_TITLE_RANDOM": "कुनै अंश", + "MATH_RANDOM_FLOAT_TOOLTIP": "०.० (समावेशी) र १.० (विशेष) बीचको कुनै अंश फर्काउनुहोस्।", + "MATH_ATAN2_TITLE": "X:%1 Y:%2 को atan2", + "MATH_ATAN2_TOOLTIP": "-180 देखि 180 डिग्रीमा पर्ने (X, Y) बिन्दु को आर्कट्यान्जेन्ट फर्काउनुहोस्।", + "TEXT_TEXT_TOOLTIP": "अक्षर, शब्द वा वाक्य रेखा।", + "TEXT_JOIN_TITLE_CREATEWITH": "यसबाट पाठ बनाउनुहोस्", + "TEXT_JOIN_TOOLTIP": "केही कुराहरू जोडेर पाठ्यांस बनाउनुहोस्।", + "TEXT_CREATE_JOIN_TITLE_JOIN": "मिलाउ", + "TEXT_CREATE_JOIN_TOOLTIP": "यो पाठ्यांस पुन: मिलाउनलाई खण्डहरू थप्नुहोस्, हटाउनुहोस् वा पुन: क्रमबद्ध गर्नुहोस्।", + "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "पाठमा एक चिज थप्नुहोस्।", + "TEXT_APPEND_TITLE": "%1 मा पाठ %2 थप्नुहोस्", + "TEXT_APPEND_TOOLTIP": "चर '%1' मा केही पाठ थप्नुहोस्।", + "TEXT_LENGTH_TITLE": "%1 को लम्बाइ", + "TEXT_LENGTH_TOOLTIP": "दिइएको पाठमा अक्षरहरूको संख्या (स्पेस सहित) फर्काउँछ।", + "TEXT_ISEMPTY_TITLE": "%1 खाली छ", + "TEXT_ISEMPTY_TOOLTIP": "यदि दिइएको पाठ खाली छ भने true फर्काउँछ।", + "TEXT_INDEXOF_TOOLTIP": "दोस्रो पाठमा पहिलो पाठ भेटिएको पहिलो/अन्तिम अनुक्रमणिका फर्काउँछ। पाठ फेला परेन भने %1 फर्काउँछ।", + "TEXT_INDEXOF_TITLE": "पाठमा %1 %2 %3", + "TEXT_INDEXOF_OPERATOR_FIRST": "पाठको पहिलो ठाँउ फेला पार्नुहोस्", + "TEXT_INDEXOF_OPERATOR_LAST": "पाठको अन्तिम ठाँउ फेला पार्नुहोस्", + "TEXT_CHARAT_TITLE": "पाठमा %1 %2", + "TEXT_CHARAT_FROM_START": "अक्षर # निकाल", + "TEXT_CHARAT_FROM_END": "अन्तबाट अक्षर # निकाल", + "TEXT_CHARAT_FIRST": "पहिलो अक्षर निकाल", + "TEXT_CHARAT_LAST": "अन्तिम अक्षर निकाल", + "TEXT_CHARAT_RANDOM": "कुनै अक्षर निकाल", + "TEXT_CHARAT_TOOLTIP": "निर्दिष्ट स्थानको अक्षर फर्काउँछ।", + "TEXT_GET_SUBSTRING_TOOLTIP": "पाठको निर्दिष्ट भाग फर्काउँछ।", + "TEXT_GET_SUBSTRING_INPUT_IN_TEXT": "पाठमा", + "TEXT_GET_SUBSTRING_START_FROM_START": "अक्षर # देखि बाँकी निकाल", + "TEXT_GET_SUBSTRING_START_FROM_END": "अन्तिमको अक्षर # बाट बाँकी निकाल", + "TEXT_GET_SUBSTRING_START_FIRST": "पहिलो अक्षरबाट बाँकी निकाल", + "TEXT_GET_SUBSTRING_END_FROM_START": "अक्षर # सम्म", + "TEXT_GET_SUBSTRING_END_FROM_END": "अन्त्यबाट # अक्षर सम्म", + "TEXT_GET_SUBSTRING_END_LAST": "अन्तिम अक्षरसम्म", + "TEXT_CHANGECASE_TOOLTIP": "फरक अवस्थामा पाठको प्रतिलिपि फर्काउनुहोस्।", + "TEXT_CHANGECASE_OPERATOR_UPPERCASE": "to UPPER CASE", + "TEXT_CHANGECASE_OPERATOR_LOWERCASE": "to lower case", + "TEXT_CHANGECASE_OPERATOR_TITLECASE": "to Title Case", + "TEXT_TRIM_TOOLTIP": "एक वा दुबै छेउबाट खाली ठाउँहरू हटाएर पाठको प्रतिलिपि फर्काउनुहोस्।", + "TEXT_TRIM_OPERATOR_BOTH": "दुबै छेउबाट खाली ठाउँहरू हटाउनुहोस्", + "TEXT_TRIM_OPERATOR_LEFT": "बायाँबाट खाली ठाउँहरू हटाउनुहोस्", + "TEXT_TRIM_OPERATOR_RIGHT": "दायाँ छेउबाट खाली ठाउँहरू हटाउनुहोस्", + "TEXT_PRINT_TITLE": "%1 छाप्नुहोस्", + "TEXT_PRINT_TOOLTIP": "निर्दिष्ट पाठ, नम्बर वा अन्य मान छाप्नुहोस्।", + "TEXT_PROMPT_TYPE_TEXT": "सन्देशको साथ पाठको लागि भन्नुहोस्", + "TEXT_PROMPT_TYPE_NUMBER": "सन्देशको साथ नम्बरको लागि भन्नुहोस्", + "TEXT_PROMPT_TOOLTIP_NUMBER": "नम्बरको लागि प्रयोगकर्तालाई भन्नुहोस्", + "TEXT_PROMPT_TOOLTIP_TEXT": "केहि पाठ को लागी प्रयोगकर्तालाई भन्नुहोस्।", + "TEXT_COUNT_MESSAGE0": "%2 मा %1 गणना गर्नुहोस्", + "TEXT_COUNT_TOOLTIP": "कति पटक केहि पाठ केहि अन्य पाठ भित्र छन्, गणना गर्नुहोस्।", + "TEXT_REPLACE_MESSAGE0": "%3 मा %2 ले %1 लाई प्रतिस्थापन गर्नुहोस्", + "TEXT_REPLACE_TOOLTIP": "केही पाठका सबै घटनाहरू केही अन्य पाठभित्र प्रतिस्थापन गर्नुहोस्।", + "TEXT_REVERSE_MESSAGE0": "%1 लाई उल्टो", + "TEXT_REVERSE_TOOLTIP": "पाठमा अक्षरहरूको क्रमलाई उल्टाउँछ।", + "LISTS_CREATE_EMPTY_TITLE": "खाली सूची बनाउनुहोस्", + "LISTS_CREATE_EMPTY_TOOLTIP": "कुनै तथ्याङ्क नराखी, ० लम्बाइको सूची फिर्ता गर्दछ", + "LISTS_CREATE_WITH_TOOLTIP": "कुनै संख्याको वस्तुहरूको सूची सिर्जना गर्नुहोस्।", + "LISTS_CREATE_WITH_INPUT_WITH": "यसबाट सूची बनाउनुहोस्", + "LISTS_CREATE_WITH_CONTAINER_TITLE_ADD": "सूची", + "LISTS_CREATE_WITH_CONTAINER_TOOLTIP": "यो ब्लकको सूची पुन: मिलाउनलाई खण्डहरू थप्नुहोस्, हटाउनुहोस् वा पुन: क्रमबद्ध गर्नुहोस्।", + "LISTS_CREATE_WITH_ITEM_TOOLTIP": "सूचीमा एउटा थप्नुहोस्।", + "LISTS_REPEAT_TOOLTIP": "दिइएको मान निर्दिष्ट सङ्ख्यामा दोहोऱ्याएर सूची सिर्जना गर्दछ।", + "LISTS_REPEAT_TITLE": "%1 लाई %2 पटक राखेर सूची बनाउनुहोस्", + "LISTS_LENGTH_TITLE": "%1 को लम्बाइ", + "LISTS_LENGTH_TOOLTIP": "सूचीको लम्बाइ फर्काउँछ।", + "LISTS_ISEMPTY_TITLE": "%1 खाली छ", + "LISTS_ISEMPTY_TOOLTIP": "यदि सूची खाली छ भने true फर्काउँछ।", + "LISTS_INLIST": "सूचीमा", + "LISTS_INDEX_OF_FIRST": "पहिलो घटना फेला पार्नुहोस्", + "LISTS_INDEX_OF_LAST": "अन्तिम घटना फेला पार्नुहोस्", + "LISTS_INDEX_OF_TOOLTIP": "सूचीबाट वस्तुको पहिलो/अन्तिम घटनाको अनुक्रमणिका फर्काउँछ। वस्तु फेला परेन भने %1 फर्काउँछ।", + "LISTS_GET_INDEX_GET": "निकाल", + "LISTS_GET_INDEX_GET_REMOVE": "निकाल र हटाउ", "PROCEDURES_MUTATORCONTAINER_TITLE": "इन्पुटहरू", "DIALOG_OK": "हुन्छ", - "DIALOG_CANCEL": "रद्द गर्नुहोस्" + "DIALOG_CANCEL": "हुदैन" } diff --git a/msg/json/nl.json b/msg/json/nl.json index 8961dc112df..914a8ef8ded 100644 --- a/msg/json/nl.json +++ b/msg/json/nl.json @@ -46,13 +46,13 @@ "RENAME_VARIABLE": "Variabele hernoemen...", "RENAME_VARIABLE_TITLE": "Alle variabelen \"%1\" hernoemen naar:", "NEW_VARIABLE": "Variabele maken...", - "NEW_STRING_VARIABLE": "Creëer tekstvariabele", - "NEW_NUMBER_VARIABLE": "Creëer numeriek variabele", - "NEW_COLOUR_VARIABLE": "Creëer kleurvariabele", - "NEW_VARIABLE_TYPE_TITLE": "Nieuw soort variabele", + "NEW_STRING_VARIABLE": "Tekstvariabele maken...", + "NEW_NUMBER_VARIABLE": "Numeriek variabele maken...", + "NEW_COLOUR_VARIABLE": "Kleurvariabele maken...", + "NEW_VARIABLE_TYPE_TITLE": "Nieuw variabeletype:", "NEW_VARIABLE_TITLE": "Nieuwe variabelenaam:", "VARIABLE_ALREADY_EXISTS": "Er bestaat al een variabele met de naam \"%1\".", - "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Een variabele met de naam '%1' bestaat al voor een ander soort variabele: '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Een variabele met de naam '%1' bestaat al voor een ander variabeletype: '%2'.", "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Een variabele met de naam '%1' bestaat al als parameter in de procedure '%2'.", "DELETE_VARIABLE_CONFIRMATION": "%1 gebruiken van de variabele \"%2\" verwijderen?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "De variabele \"%1\" kan niet verwijderd worden omdat die onderdeel uitmaakt van de definitie van de functie \"%2\"", @@ -196,7 +196,7 @@ "MATH_RANDOM_FLOAT_TITLE_RANDOM": "willekeurige fractie", "MATH_RANDOM_FLOAT_TOOLTIP": "Geeft een willekeurige fractie tussen 0.0 (inclusief) en 1.0 (exclusief).", "MATH_ATAN2_TITLE": "atan2 van X:%1 Y:%2", - "MATH_ATAN2_TOOLTIP": "Geef de boogtangens van punt (X, Y) terug in graden tussen -180 naar 180.", + "MATH_ATAN2_TOOLTIP": "Geef de boogtangens van punt (X, Y) terug in graden tussen -180 en 180.", "TEXT_TEXT_HELPURL": "https://nl.wikipedia.org/wiki/Tekenreeks", "TEXT_TEXT_TOOLTIP": "Een letter, woord of een regel tekst.", "TEXT_JOIN_TITLE_CREATEWITH": "maak tekst met", @@ -204,7 +204,7 @@ "TEXT_CREATE_JOIN_TITLE_JOIN": "samenvoegen", "TEXT_CREATE_JOIN_TOOLTIP": "Toevoegen, verwijderen of volgorde wijzigen van secties om dit tekstblok opnieuw in te stellen.", "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "Voegt een item aan de tekst toe.", - "TEXT_APPEND_TITLE": "voor%1 voeg tekst toe van %2", + "TEXT_APPEND_TITLE": "voor %1 voeg tekst toe: %2", "TEXT_APPEND_TOOLTIP": "Voeg tekst toe aan de variabele \"%1\".", "TEXT_LENGTH_TITLE": "lengte van %1", "TEXT_LENGTH_TOOLTIP": "Geeft het aantal tekens terug (inclusief spaties) in de opgegeven tekst.", diff --git a/msg/json/pt-br.json b/msg/json/pt-br.json index 895f3ec414a..2754c603937 100644 --- a/msg/json/pt-br.json +++ b/msg/json/pt-br.json @@ -14,6 +14,7 @@ "Fasouzafreitas", "Felipe L. Ewald", "Glaucia Japui Gonçalves", + "Lc97", "Lowvy", "Luk3", "Mauricio", @@ -62,13 +63,13 @@ "NEW_VARIABLE_TYPE_TITLE": "Tipo da nova variável:", "NEW_VARIABLE_TITLE": "Nome da nova variável:", "VARIABLE_ALREADY_EXISTS": "A variável chamada '%1' já existe.", - "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Já existe uma variável chamada '%1' para outra do tipo: '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Já existe uma variável chamada '%1' para outro tipo: '%2'.", "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Já existe uma variável chamada '%1' como parâmetro no procedimento '%2", "DELETE_VARIABLE_CONFIRMATION": "Deletar %1 usos da variável '%2'?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Não se pode deletar a variável '%1' porque é parte da definição da função '%2'", "DELETE_VARIABLE": "Deletar a variável '%1'", "COLOUR_PICKER_HELPURL": "https://pt.wikipedia.org/wiki/Cor", - "COLOUR_PICKER_TOOLTIP": "Escolher uma cor da palheta de cores.", + "COLOUR_PICKER_TOOLTIP": "Escolher uma cor da palheta.", "COLOUR_RANDOM_TITLE": "cor aleatória", "COLOUR_RANDOM_TOOLTIP": "Escolher cor de forma aleatória.", "COLOUR_RGB_TITLE": "colorir com", diff --git a/msg/json/qqq.json b/msg/json/qqq.json index 60509d798e2..fcd8897bd04 100644 --- a/msg/json/qqq.json +++ b/msg/json/qqq.json @@ -53,7 +53,7 @@ "COLOUR_RANDOM_HELPURL": "{{Optional}} url - A link that displays a random colour each time you visit it.", "COLOUR_RANDOM_TITLE": "block text - Title of block that generates a colour at random.", "COLOUR_RANDOM_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Colour#generating-a-random-colour https://github.com/google/blockly/wiki/Colour#generating-a-random-colour].", - "COLOUR_RGB_HELPURL": "{{Ignored}} url - A link for colour codes with percentages (0-100%) for each component, instead of the more common 0-255, which may be more difficult for beginners.", + "COLOUR_RGB_HELPURL": "{{Optional}} url - A link for colour codes with percentages (0-100%) for each component, instead of the more common 0-255, which may be more difficult for beginners.", "COLOUR_RGB_TITLE": "block text - Title of block for [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].", "COLOUR_RGB_RED": "block input text - The amount of red (from 0 to 100) to use when [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].\n{{Identical|Red}}", "COLOUR_RGB_GREEN": "block input text - The amount of green (from 0 to 100) to use when [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].", @@ -250,7 +250,7 @@ "TEXT_GET_SUBSTRING_END_FROM_START": "dropdown - Indicates that the following number specifies the position (relative to the start position) of the end of the region of text that should be obtained from the preceding piece of text. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", "TEXT_GET_SUBSTRING_END_FROM_END": "dropdown - Indicates that the following number specifies the position (relative to the end position) of the end of the region of text that should be obtained from the preceding piece of text. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", "TEXT_GET_SUBSTRING_END_LAST": "block text - Indicates that a region ending with the last letter of the preceding piece of text should be extracted. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", - "TEXT_GET_SUBSTRING_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}}\nblock text - Text that should go after the rightmost block/dropdown when [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text extracting a region of text]. In most languages, this will be the empty string. [[File:Blockly-get-substring.png]]", + "TEXT_GET_SUBSTRING_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - Text that should go after the rightmost block/dropdown when [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text extracting a region of text]. In most languages, this will be the empty string. [[File:Blockly-get-substring.png]]", "TEXT_CHANGECASE_HELPURL": "{{Optional}} url - Information about the case of letters (upper-case and lower-case).", "TEXT_CHANGECASE_TOOLTIP": "tooltip - Describes a block to adjust the case of letters. For more information on this block, see [https://github.com/google/blockly/wiki/Text#adjusting-text-case https://github.com/google/blockly/wiki/Text#adjusting-text-case].", "TEXT_CHANGECASE_OPERATOR_UPPERCASE": "block text - Indicates that all of the letters in the following piece of text should be capitalized. If your language does not use case, you may indicate that this is not applicable to your language. For more information on this block, see [https://github.com/google/blockly/wiki/Text#adjusting-text-case https://github.com/google/blockly/wiki/Text#adjusting-text-case].", @@ -297,10 +297,11 @@ "LISTS_ISEMPTY_TITLE": "block text - See [https://github.com/google/blockly/wiki/Lists#is-empty https://github.com/google/blockly/wiki/Lists#is-empty]. \n\nParameters:\n* %1 - the list to test", "LISTS_ISEMPTY_TOOLTIP": "block tooltip - See [https://github.com/google/blockly/wiki/Lists#is-empty https://github.com/google/blockly/wiki/Lists#is-empty].", "LISTS_INLIST": "block text - Title of blocks operating on [https://github.com/google/blockly/wiki/Lists lists].", - "LISTS_INDEX_OF_HELPURL": "{{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list].", + "LISTS_INDEX_OF_HELPURL": "{{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list].", "LISTS_INDEX_OF_FIRST": "dropdown - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list Lists#finding-items-in-a-list]. [[File:Blockly-list-find.png]]", "LISTS_INDEX_OF_LAST": "dropdown - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list]. [[File:Blockly-list-find.png]]", "LISTS_INDEX_OF_TOOLTIP": "tooltip - %1 will be replaced by either the number 0 or -1 depending on the indexing mode. See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list]. [[File:Blockly-list-find.png]]", + "LISTS_GET_INDEX_HELPURL": "{{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list].", "LISTS_GET_INDEX_GET": "dropdown - Indicates that the user wishes to [https://github.com/google/blockly/wiki/Lists#getting-a-single-item get an item from a list] without removing it from the list.", "LISTS_GET_INDEX_GET_REMOVE": "dropdown - Indicates that the user wishes to [https://github.com/google/blockly/wiki/Lists#getting-a-single-item get and remove an item from a list], as opposed to merely getting it without modifying the list.", "LISTS_GET_INDEX_REMOVE": "dropdown - Indicates that the user wishes to [https://github.com/google/blockly/wiki/Lists#removing-an-item remove an item from a list].\n{{Identical|Remove}}", @@ -309,7 +310,7 @@ "LISTS_GET_INDEX_FIRST": "dropdown - Indicates that the '''first''' item should be [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessed in a list]. [[File:Blockly-list-get-item.png]]", "LISTS_GET_INDEX_LAST": "dropdown - Indicates that the '''last''' item should be [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessed in a list]. [[File:Blockly-list-get-item.png]]", "LISTS_GET_INDEX_RANDOM": "dropdown - Indicates that a '''random''' item should be [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessed in a list]. [[File:Blockly-list-get-item.png]]", - "LISTS_GET_INDEX_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}}\n\nblock text - Text that should go after the rightmost block/dropdown when [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessing an item from a list]. In most languages, this will be the empty string. [[File:Blockly-list-get-item.png]]", + "LISTS_GET_INDEX_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - Text that should go after the rightmost block/dropdown when [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessing an item from a list]. In most languages, this will be the empty string. [[File:Blockly-list-get-item.png]]", "LISTS_INDEX_FROM_START_TOOLTIP": "tooltip - Indicates the ordinal number that the first item in a list is referenced by. %1 will be replaced by either '#0' or '#1' depending on the indexing mode.", "LISTS_INDEX_FROM_END_TOOLTIP": "tooltip - Indicates the ordinal number that the last item in a list is referenced by. %1 will be replaced by either '#0' or '#1' depending on the indexing mode.", "LISTS_GET_INDEX_TOOLTIP_GET_FROM": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for more information.", @@ -343,7 +344,7 @@ "LISTS_GET_SUBLIST_END_FROM_START": "dropdown - Indicates that an index relative to the front of the list should be used to specify the end of the range from which to [https://github.com/google/blockly/wiki/Lists#getting-a-sublist get a sublist]. [[File:Blockly-get-sublist.png]]", "LISTS_GET_SUBLIST_END_FROM_END": "dropdown - Indicates that an index relative to the end of the list should be used to specify the end of the range from which to [https://github.com/google/blockly/wiki/Lists#getting-a-sublist get a sublist]. [[File:Blockly-get-sublist.png]]", "LISTS_GET_SUBLIST_END_LAST": "dropdown - Indicates that the '''last''' item in the given list should be [https://github.com/google/blockly/wiki/Lists#getting-a-sublist the end of the selected sublist]. [[File:Blockly-get-sublist.png]]", - "LISTS_GET_SUBLIST_TAIL": "{{Optional}}\nblock text - This appears in the rightmost position ('tail') of the sublist block, as described at [https://github.com/google/blockly/wiki/Lists#getting-a-sublist https://github.com/google/blockly/wiki/Lists#getting-a-sublist]. In English and most other languages, this is the empty string. [[File:Blockly-get-sublist.png]]", + "LISTS_GET_SUBLIST_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - This appears in the rightmost position ('tail') of the sublist block, as described at [https://github.com/google/blockly/wiki/Lists#getting-a-sublist https://github.com/google/blockly/wiki/Lists#getting-a-sublist]. In English and most other languages, this is the empty string. [[File:Blockly-get-sublist.png]]", "LISTS_GET_SUBLIST_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-sublist https://github.com/google/blockly/wiki/Lists#getting-a-sublist] for more information. [[File:Blockly-get-sublist.png]]", "LISTS_SORT_HELPURL": "{{Optional}} url - Information describing sorting a list.", "LISTS_SORT_TITLE": "Sort as type %1 (numeric or alphabetic) in order %2 (ascending or descending) a list of items %3.\n{{Identical|Sort}}", @@ -362,7 +363,7 @@ "LISTS_REVERSE_HELPURL": "{{Optional}} url - Information describing reversing a list.", "LISTS_REVERSE_MESSAGE0": "block text - Title of block that returns a copy of a list (%1) with the order of items reversed.", "LISTS_REVERSE_TOOLTIP": "tooltip - Short description for a block that reverses a copy of a list.", - "ORDINAL_NUMBER_SUFFIX": "{{Optional}}\ngrammar - Text that follows an ordinal number (a number that indicates position relative to other numbers). In most languages, such text appears before the number, so this should be blank. An exception is Hungarian. See [[Translating:Blockly#Ordinal_numbers]] for more information.", + "ORDINAL_NUMBER_SUFFIX": "{{Optional|Supply translation only if your language requires it. Most do not.}} grammar - Text that follows an ordinal number (a number that indicates position relative to other numbers). In most languages, such text appears before the number, so this should be blank. An exception is Hungarian. See [[Translating:Blockly#Ordinal_numbers]] for more information.", "VARIABLES_GET_HELPURL": "{{Optional}} url - Information about ''variables'' in computer programming. Consider using your language's translation of [https://en.wikipedia.org/wiki/Variable_(computer_science) https://en.wikipedia.org/wiki/Variable_(computer_science)], if it exists.", "VARIABLES_GET_TOOLTIP": "tooltip - This gets the value of the named variable without modifying it.", "VARIABLES_GET_CREATE_SET": "context menu - Selecting this creates a block to set (change) the value of this variable. \n\nParameters:\n* %1 - the name of the variable.", @@ -375,7 +376,8 @@ "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_DEFNORETURN_DO": "{{Optional}}\nblock 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_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.", "PROCEDURES_DEFRETURN_HELPURL": "{{Optional}} url - Information about defining [https://en.wikipedia.org/wiki/Subroutine functions] that have return values.", diff --git a/msg/json/ru.json b/msg/json/ru.json index bf9f36ff178..b242563b859 100644 --- a/msg/json/ru.json +++ b/msg/json/ru.json @@ -118,7 +118,7 @@ "LOGIC_BOOLEAN_TOOLTIP": "Возвращает значение истина или ложь.", "LOGIC_NULL": "ничто", "LOGIC_NULL_TOOLTIP": "Возвращает ничто.", - "LOGIC_TERNARY_HELPURL": "https://ru.wikipedia.org/wiki/Тернарная_условная_операция", + "LOGIC_TERNARY_HELPURL": "https://ru.wikipedia.org/wiki/%3F#В_компьютерах", "LOGIC_TERNARY_CONDITION": "выбрать по", "LOGIC_TERNARY_IF_TRUE": "если истина", "LOGIC_TERNARY_IF_FALSE": "если ложь", @@ -270,6 +270,7 @@ "LISTS_INDEX_OF_FIRST": "найти первое вхождение элемента", "LISTS_INDEX_OF_LAST": "найти последнее вхождение элемента", "LISTS_INDEX_OF_TOOLTIP": "Возвращает номер позиции первого/последнего вхождения элемента в списке. Возвращает %1, если элемент не найден.", + "LISTS_GET_INDEX_HELPURL": "https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list", "LISTS_GET_INDEX_GET": "взять", "LISTS_GET_INDEX_GET_REMOVE": "взять и удалить", "LISTS_GET_INDEX_REMOVE": "удалить", @@ -334,6 +335,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "выполнить что-то", "PROCEDURES_BEFORE_PARAMS": "с:", "PROCEDURES_CALL_BEFORE_PARAMS": "с:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Невозможно запустить пользовательскую функцию '%1', поскольку блок определения отключен.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Создаёт процедуру, не возвращающую значение.", "PROCEDURES_DEFNORETURN_COMMENT": "Опишите эту функцию…", "PROCEDURES_DEFRETURN_HELPURL": "https://ru.wikipedia.org/wiki/Функция_(программирование)", diff --git a/msg/json/synonyms.json b/msg/json/synonyms.json index d7a05c3ab15..9fc089ebea7 100644 --- a/msg/json/synonyms.json +++ b/msg/json/synonyms.json @@ -1 +1,22 @@ -{"CONTROLS_FOREACH_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", "CONTROLS_FOR_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", "CONTROLS_IF_ELSEIF_TITLE_ELSEIF": "CONTROLS_IF_MSG_ELSEIF", "CONTROLS_IF_ELSE_TITLE_ELSE": "CONTROLS_IF_MSG_ELSE", "CONTROLS_IF_IF_TITLE_IF": "CONTROLS_IF_MSG_IF", "CONTROLS_IF_MSG_THEN": "CONTROLS_REPEAT_INPUT_DO", "CONTROLS_WHILEUNTIL_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", "LISTS_CREATE_WITH_ITEM_TITLE": "VARIABLES_DEFAULT_NAME", "LISTS_GET_INDEX_HELPURL": "LISTS_INDEX_OF_HELPURL", "LISTS_GET_INDEX_INPUT_IN_LIST": "LISTS_INLIST", "LISTS_GET_SUBLIST_INPUT_IN_LIST": "LISTS_INLIST", "LISTS_INDEX_OF_INPUT_IN_LIST": "LISTS_INLIST", "LISTS_SET_INDEX_INPUT_IN_LIST": "LISTS_INLIST", "MATH_CHANGE_TITLE_ITEM": "VARIABLES_DEFAULT_NAME", "PROCEDURES_DEFRETURN_COMMENT": "PROCEDURES_DEFNORETURN_COMMENT", "PROCEDURES_DEFRETURN_DO": "PROCEDURES_DEFNORETURN_DO", "PROCEDURES_DEFRETURN_PROCEDURE": "PROCEDURES_DEFNORETURN_PROCEDURE", "PROCEDURES_DEFRETURN_TITLE": "PROCEDURES_DEFNORETURN_TITLE", "TEXT_APPEND_VARIABLE": "VARIABLES_DEFAULT_NAME", "TEXT_CREATE_JOIN_ITEM_TITLE_ITEM": "VARIABLES_DEFAULT_NAME"} \ No newline at end of file +{ + "#": "Automatically generated, do not edit this file!", + "CONTROLS_FOREACH_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", + "CONTROLS_FOR_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", + "CONTROLS_IF_ELSEIF_TITLE_ELSEIF": "CONTROLS_IF_MSG_ELSEIF", + "CONTROLS_IF_ELSE_TITLE_ELSE": "CONTROLS_IF_MSG_ELSE", + "CONTROLS_IF_IF_TITLE_IF": "CONTROLS_IF_MSG_IF", + "CONTROLS_IF_MSG_THEN": "CONTROLS_REPEAT_INPUT_DO", + "CONTROLS_WHILEUNTIL_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", + "LISTS_CREATE_WITH_ITEM_TITLE": "VARIABLES_DEFAULT_NAME", + "LISTS_GET_INDEX_INPUT_IN_LIST": "LISTS_INLIST", + "LISTS_GET_SUBLIST_INPUT_IN_LIST": "LISTS_INLIST", + "LISTS_INDEX_OF_INPUT_IN_LIST": "LISTS_INLIST", + "LISTS_SET_INDEX_INPUT_IN_LIST": "LISTS_INLIST", + "MATH_CHANGE_TITLE_ITEM": "VARIABLES_DEFAULT_NAME", + "PROCEDURES_DEFRETURN_COMMENT": "PROCEDURES_DEFNORETURN_COMMENT", + "PROCEDURES_DEFRETURN_DO": "PROCEDURES_DEFNORETURN_DO", + "PROCEDURES_DEFRETURN_PROCEDURE": "PROCEDURES_DEFNORETURN_PROCEDURE", + "PROCEDURES_DEFRETURN_TITLE": "PROCEDURES_DEFNORETURN_TITLE", + "TEXT_APPEND_VARIABLE": "VARIABLES_DEFAULT_NAME", + "TEXT_CREATE_JOIN_ITEM_TITLE_ITEM": "VARIABLES_DEFAULT_NAME" +} \ No newline at end of file diff --git a/msg/json/ta.json b/msg/json/ta.json index 9eb44de129d..5138bc26c4f 100644 --- a/msg/json/ta.json +++ b/msg/json/ta.json @@ -7,7 +7,8 @@ "Fahimrazick", "Karuthan", "Mahir78", - "Thangamani-arun" + "Thangamani-arun", + "தமிழ்நேரம்" ] }, "VARIABLES_DEFAULT_NAME": "உருப்படி", @@ -16,11 +17,13 @@ "DUPLICATE_BLOCK": "மறுநகல்", "ADD_COMMENT": "கருத்தை சேர்", "REMOVE_COMMENT": "கருத்தை நீக்கு", + "DUPLICATE_COMMENT": "கருத்தை நகலெடு", "EXTERNAL_INPUTS": "வெளி கருவிகளுடன் உள்ளீடு", "INLINE_INPUTS": "சூழமைவில் உள்ளீடு", "DELETE_BLOCK": "உறுப்பை நீக்கு", "DELETE_X_BLOCKS": "%1 உறுப்பை நீக்கு", "DELETE_ALL_BLOCKS": "அனைத்து %1 நிரல் துண்டுகளையும் அழிக்கவா??", + "CLEAN_UP": "தொகுதிகளைச் சுத்தம் செய்", "COLLAPSE_BLOCK": "உறுப்பை மரை", "COLLAPSE_ALL": "உறுப்புகளை மரை", "EXPAND_BLOCK": "உறுப்பை காட்டு", @@ -34,8 +37,12 @@ "RENAME_VARIABLE": "மாறிலியை மறுபெயரிடுக...", "RENAME_VARIABLE_TITLE": "அனைத்து '%1' மாறிலிகளையும் பின்வருமாறு மறுபெயரிடுக:", "NEW_VARIABLE": "மாறிலியை உருவாக்குக...", + "NEW_STRING_VARIABLE": "சரம் மாறியை உருவாக்கு...", + "NEW_NUMBER_VARIABLE": "எண் மாறியை உருவாக்கு...", + "NEW_COLOUR_VARIABLE": "வண்ண மாறியை உருவாக்கு...", "NEW_VARIABLE_TYPE_TITLE": "புதிய மாறிலியின் பெயர்:", "NEW_VARIABLE_TITLE": "புதிய மாறிலியின் பெயர்:", + "VARIABLE_ALREADY_EXISTS": "'%1' என்ற பெயர் கொண்ட ஒரு மாறி ஏற்கனவே உள்ளது.", "COLOUR_PICKER_TOOLTIP": "வண்ண தட்டிலிருந்து ஒரு நிறத்தைத் தேர்ந்தெடுக்கவும்.", "COLOUR_RANDOM_TITLE": "தற்போக்கு நிறம்", "COLOUR_RANDOM_TOOLTIP": "தற்போக்கில் ஒரு நிறத்தை தேர்ந்தெடுக்கவும்.", diff --git a/msg/json/tdd.json b/msg/json/tdd.json index 2be90564cb1..2bb756d2367 100644 --- a/msg/json/tdd.json +++ b/msg/json/tdd.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "AeyTaiNuea", "咽頭べさ" ] }, @@ -71,7 +72,7 @@ "LOGIC_COMPARE_TOOLTIP_GT": "ᥔᥒᥴᥝᥣᥲ ᥟᥢᥴᥚᥫᥛᥳᥔᥬᥱ ᥟᥩᥢᥴᥖᥣᥒᥰ ᥕᥬᥱᥘᥫᥴ ᥟᥢᥴᥚᥫᥛᥳᥔᥬᥱ ᥛᥣᥭᥴᥔᥩᥒᥴᥓᥪᥒᥴ ᥛᥦᥰᥑᥪᥢᥰ ᥞᥬᥲᥛᥣᥢᥱᥛᥦᥢᥲ.", "LOGIC_COMPARE_TOOLTIP_GTE": "ᥔᥒᥴᥝᥣᥲ ᥟᥢᥴᥚᥫᥛᥳᥔᥬᥱ ᥟᥩᥢᥴᥖᥣᥒᥰ ᥕᥬᥱᥘᥫᥴ ᥟᥛᥱᥢᥢᥴ ᥚᥥᥒᥱᥙᥥᥒᥰ ᥟᥢᥴᥚᥫᥛᥳᥔᥬᥱ ᥛᥣᥭᥴᥔᥩᥒᥴᥓᥪᥒᥴ ᥛᥦᥰᥑᥪᥢᥰ ᥞᥬᥲᥛᥣᥢᥱᥛᥦᥢᥲ.", "LOGIC_OPERATION_TOOLTIP_AND": "ᥔᥒᥴᥝᥣᥲ ᥟᥢᥴᥚᥫᥛᥳᥔᥬᥱ ᥖᥒᥰᥔᥩᥒᥴᥟᥢᥴ ᥛᥣᥢᥱᥛᥦᥢᥲᥓᥪᥒᥴᥓᥪᥒᥴ ᥛᥦᥰᥑᥪᥢᥰ ᥞᥬᥲᥛᥣᥢᥱᥛᥦᥢᥲ.", - "LOGIC_OPERATION_AND": "ᥘᥦᥲ", + "LOGIC_OPERATION_AND": "ᥘᥦᥰ", "LOGIC_OPERATION_TOOLTIP_OR": "ᥔᥒᥴᥝᥣᥲ ᥟᥢᥴᥚᥫᥛᥳᥔᥬᥱ ᥐᥛᥰᥘᥪᥢᥰᥔᥧᥖᥰ ᥛᥣᥢᥱᥛᥦᥢᥲᥓᥪᥒᥴ ᥛᥦᥰᥑᥪᥢᥰ ᥞᥬᥲᥛᥣᥢᥱᥛᥦᥢᥲ.", "LOGIC_OPERATION_OR": "ᥟᥛᥱᥢᥢᥴ", "LOGIC_NEGATE_TITLE": "ᥟᥛᥱᥓᥬᥲ %1", @@ -101,8 +102,8 @@ "MATH_SINGLE_TOOLTIP_NEG": "ᥘᥥᥝᥴᥑᥪᥢᥰ ᥟᥢᥴᥔᥣᥢᥴᥑᥖᥰ ᥢᥬᥰ ᥛᥣᥭᥴᥢᥙᥳ.", "MATH_SINGLE_TOOLTIP_LN": "ᥘᥥᥝᥴᥑᥪᥢᥰ ᥘᥩᥐᥰᥘᥣᥭᥰᥢᥙᥳ ᥢᥬᥰᥛᥣᥭᥴᥢᥙᥳ.", "MATH_SINGLE_TOOLTIP_LOG10": "ᥘᥥᥝᥴᥑᥪᥢᥰ ᥙᥪᥢᥳᥗᥣᥢᥴ 10 ᥘᥩᥐᥰᥘᥣᥭᥰᥢᥙᥳ ᥢᥬᥰᥛᥣᥭᥴᥢᥙᥳ.", - "MATH_SINGLE_TOOLTIP_EXP": "ᥘᥥᥝᥴᥑᥪᥢᥰ e ᥐᥣᥱᥖᥤᥲ ᥙᥣᥱᥝᥣᥱ ᥢᥬᥰᥛᥣᥭᥴᥢᥙᥳ.", - "MATH_SINGLE_TOOLTIP_POW10": "ᥘᥥᥝᥴᥑᥪᥢᥰ 10 ᥐᥣᥱᥖᥤᥲ ᥙᥣᥱᥝᥣᥱ ᥢᥬᥰᥛᥣᥭᥴᥢᥙᥳ.", + "MATH_SINGLE_TOOLTIP_EXP": "ᥘᥥᥝᥴ ᥑᥪᥢᥰ e ᥐᥣᥱ ᥖᥤ ᥙᥣᥱ ᥝᥣᥱ ᥘᥬᥰ ᥛᥣᥭᥴ ᥘᥙᥳ.", + "MATH_SINGLE_TOOLTIP_POW10": "ᥘᥥᥝᥴ ᥑᥪᥢᥰ 10 ᥐᥣᥱ ᥖᥤ ᥙᥣᥱ ᥝᥣᥱ ᥘᥬᥰ ᥛᥣᥭᥴ ᥘᥙᥳ.", "MATH_IS_EVEN": "ᥙᥥᥢᥴᥐᥨᥙᥳ", "MATH_IS_ODD": "ᥙᥥᥢᥴᥐᥤᥐᥲ" } diff --git a/msg/json/tr.json b/msg/json/tr.json index a31acf1432e..b65a5ce31f7 100644 --- a/msg/json/tr.json +++ b/msg/json/tr.json @@ -17,6 +17,7 @@ "McAang", "Meelo", "MuratTheTurkish", + "RuzDD", "Stonecy", "Uncitoyen", "Uğurkent", @@ -28,7 +29,7 @@ "VARIABLES_DEFAULT_NAME": "öge", "UNNAMED_KEY": "isimsiz", "TODAY": "Bugün", - "DUPLICATE_BLOCK": "Yinelenen", + "DUPLICATE_BLOCK": "Yinele", "ADD_COMMENT": "Yorum Ekle", "REMOVE_COMMENT": "Yorumu Sil", "DUPLICATE_COMMENT": "Yinelenen Yorum", @@ -51,7 +52,7 @@ "RENAME_VARIABLE": "Değişkeni yeniden adlandır...", "RENAME_VARIABLE_TITLE": "Tüm '%1' değişkenini yeniden adlandır:", "NEW_VARIABLE": "Değişken oluştur...", - "NEW_STRING_VARIABLE": "Dizi değişkeni oluştur...", + "NEW_STRING_VARIABLE": "Karakter dizisi değişkeni oluştur...", "NEW_NUMBER_VARIABLE": "Sayı değişkeni oluştur...", "NEW_COLOUR_VARIABLE": "Renk değişkeni oluştur...", "NEW_VARIABLE_TYPE_TITLE": "Yeni değişken tipi:", diff --git a/msg/json/vi.json b/msg/json/vi.json index 8585dded90e..5ebfc92301f 100644 --- a/msg/json/vi.json +++ b/msg/json/vi.json @@ -9,6 +9,7 @@ "Nguyenvanduocit", "Nguyễn Mạnh An", "Qneutron", + "Quangkhanhhuynh", "SierraNguyen", "TARGET6tidiem", "Withoutaname" @@ -47,6 +48,7 @@ "NEW_VARIABLE_TITLE": "Tên của biến mới:", "VARIABLE_ALREADY_EXISTS": "Một biến có tên '%1' đã tồn tại.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Giá trị '%1' đã tồn tại dưới dạng: '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Một biến có tên '%1' đã tồn tại dưới dạng tham số trong quy trình '%2'.", "DELETE_VARIABLE_CONFIRMATION": "Xóa %1 lần dùng của giá trị '%2'?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Không thể xóa giá trị '%1' vì nó được bao gồm trong định nghĩa của chức năng '%2'", "DELETE_VARIABLE": "Xóa giá trị '%1'", @@ -324,9 +326,9 @@ "PROCEDURES_ALLOW_STATEMENTS": "cho phép báo cáo", "PROCEDURES_DEF_DUPLICATE_WARNING": "Chú ý: Thủ tục này có lặp lại tên các tham số.", "PROCEDURES_CALLNORETURN_HELPURL": "https://vi.wikipedia.org/wiki/Ch%C6%B0%C6%A1ng_tr%C3%ACnh_con", - "PROCEDURES_CALLNORETURN_TOOLTIP": "Chạy một thủ tục không có giá trị hoàn trả.", + "PROCEDURES_CALLNORETURN_TOOLTIP": "Thực hiện hàm được xác định bởi người dùng '%1'.", "PROCEDURES_CALLRETURN_HELPURL": "https://vi.wikipedia.org/wiki/Ch%C6%B0%C6%A1ng_tr%C3%ACnh_con", - "PROCEDURES_CALLRETURN_TOOLTIP": "Chạy một thủ tục có giá trị hoàn trả.", + "PROCEDURES_CALLRETURN_TOOLTIP": "Thực hiện hàm được xác định bởi người dùng '%1' và sử dụng đầu ra của nó.", "PROCEDURES_MUTATORCONTAINER_TITLE": "các tham số", "PROCEDURES_MUTATORCONTAINER_TOOLTIP": "Thêm, xóa hoặc sắp xếp lại các đầu vào cho hàm này.", "PROCEDURES_MUTATORARG_TITLE": "biến:", diff --git a/msg/json/zh-hans.json b/msg/json/zh-hans.json index 70e48beb11a..7bb6b4d50a9 100644 --- a/msg/json/zh-hans.json +++ b/msg/json/zh-hans.json @@ -4,6 +4,7 @@ ")8", "A Chinese Wikipedian", "Ambeta", + "Anterdc99", "DGCK81LNN", "Deathkon", "Duzc2", @@ -24,6 +25,7 @@ "WindWood", "Xiaomingyan", "Yfdyh000", + "人间百态", "佛壁灯", "沈澄心", "아라" @@ -125,7 +127,7 @@ "LOGIC_BOOLEAN_TOOLTIP": "返回 true 或 false。", "LOGIC_NULL": "空", "LOGIC_NULL_TOOLTIP": "返回空值。", - "LOGIC_TERNARY_HELPURL": "https://zh.wikipedia.org/wiki/条件运算符", + "LOGIC_TERNARY_HELPURL": "https://zh.wikipedia.org/wiki/条件运算符%3F:", "LOGIC_TERNARY_CONDITION": "断言", "LOGIC_TERNARY_IF_TRUE": "如果为 true", "LOGIC_TERNARY_IF_FALSE": "如果为 false", @@ -278,6 +280,7 @@ "LISTS_INDEX_OF_FIRST": "寻找第一次出现的项", "LISTS_INDEX_OF_LAST": "寻找最后一次出现的项", "LISTS_INDEX_OF_TOOLTIP": "返回在列表中的第一/最后一个匹配项的索引值。如果找不到项目则返回%1。", + "LISTS_GET_INDEX_HELPURL": "https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list", "LISTS_GET_INDEX_GET": "取得", "LISTS_GET_INDEX_GET_REMOVE": "取得并移除", "LISTS_GET_INDEX_REMOVE": "移除", diff --git a/msg/json/zh-hant.json b/msg/json/zh-hant.json index 167422e71a1..16084e23b1c 100644 --- a/msg/json/zh-hant.json +++ b/msg/json/zh-hant.json @@ -11,6 +11,7 @@ "Kly", "LNDDYL", "Liuxinyu970226", + "Sdovan1", "Sean0115", "Wehwei", "列维劳德", @@ -22,8 +23,8 @@ "VARIABLES_DEFAULT_NAME": "項目", "UNNAMED_KEY": "未命名", "TODAY": "今天", - "DUPLICATE_BLOCK": "重複", - "ADD_COMMENT": "加入註解", + "DUPLICATE_BLOCK": "再製", + "ADD_COMMENT": "添加評論", "REMOVE_COMMENT": "移除註解", "DUPLICATE_COMMENT": "複製註解", "EXTERNAL_INPUTS": "外部輸入", @@ -88,28 +89,28 @@ "CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE": "跳過這個循環的其餘步驟,並繼續下一次的循環。", "CONTROLS_FLOW_STATEMENTS_WARNING": "警告:此區塊僅可用於循環內。", "CONTROLS_IF_TOOLTIP_1": "當值為 true 時,執行一些陳述式。", - "CONTROLS_IF_TOOLTIP_2": "值為是(true)就執行第一塊陳述式,否則執行第二塊。", + "CONTROLS_IF_TOOLTIP_2": "如果值為 true 就執行第一塊陳述式,否則執行第二塊。", "CONTROLS_IF_TOOLTIP_3": "如果第一個值為 true,則執行第一塊陳述式。否則,當第二個值為 true 時,則執行第二塊陳述式。", - "CONTROLS_IF_TOOLTIP_4": "如果第一個值為 true,則執行第一塊陳述式。否則當第二個值為 true 時,則執行第二塊陳述式。如果前幾個敘述都不為 ture,則執行最後一塊陳述式。", + "CONTROLS_IF_TOOLTIP_4": "如果第一個值為 true,則執行第一塊陳述式。否則當第二個值為 true 時,則執行第二塊陳述式。如果前幾個敘述都不為 true,則執行最後一塊陳述式。", "CONTROLS_IF_MSG_IF": "如果", "CONTROLS_IF_MSG_ELSEIF": "否則,如果", "CONTROLS_IF_MSG_ELSE": "否則", "CONTROLS_IF_IF_TOOLTIP": "添加、刪除或重新排列各部份以重新配置這個「如果」區塊。", "CONTROLS_IF_ELSEIF_TOOLTIP": "添加條件到「如果」區塊。", - "CONTROLS_IF_ELSE_TOOLTIP": "加入一個最終、所有條件都執行的部份到「如果」區塊中。", + "CONTROLS_IF_ELSE_TOOLTIP": "添加一個最終、所有條件都執行的部份到「如果」區塊中。", "LOGIC_COMPARE_HELPURL": "https://zh.wikipedia.org/wiki/不等", - "LOGIC_COMPARE_TOOLTIP_EQ": "如果這兩個輸入的結果相等,返回 true。", - "LOGIC_COMPARE_TOOLTIP_NEQ": "如果這兩個輸入的結果不相等,返回 true。", + "LOGIC_COMPARE_TOOLTIP_EQ": "如果兩個輸入的結果相等,返回 true。", + "LOGIC_COMPARE_TOOLTIP_NEQ": "如果兩個輸入的結果不相等,返回 true。", "LOGIC_COMPARE_TOOLTIP_LT": "如果第一個輸入結果比第二個小,返回 true。", "LOGIC_COMPARE_TOOLTIP_LTE": "如果第一個輸入結果小於或等於第二個,返回 true。", "LOGIC_COMPARE_TOOLTIP_GT": "如果第一個輸入結果大於第二個,返回 true。", "LOGIC_COMPARE_TOOLTIP_GTE": "如果第一個輸入結果大於或等於第二個,返回 true。", "LOGIC_OPERATION_TOOLTIP_AND": "如果兩個輸入結果都為 true,則返回 true。", - "LOGIC_OPERATION_AND": "和", + "LOGIC_OPERATION_AND": "且", "LOGIC_OPERATION_TOOLTIP_OR": "如果至少一個輸入結果為 true,返回 true。", "LOGIC_OPERATION_OR": "或", "LOGIC_NEGATE_TITLE": "%1 不成立", - "LOGIC_NEGATE_TOOLTIP": "如果輸入結果是 false,則返回 true。如果輸入結果是 true,則返回 false。", + "LOGIC_NEGATE_TOOLTIP": "如果輸入結果是 false,則返回 true;如果輸入結果是 true,則返回 false。", "LOGIC_BOOLEAN_TRUE": "真", "LOGIC_BOOLEAN_FALSE": "假", "LOGIC_BOOLEAN_TOOLTIP": "返回真或假。", @@ -203,7 +204,7 @@ "TEXT_TEXT_TOOLTIP": "一個字母、一個字詞或一行字", "TEXT_JOIN_TITLE_CREATEWITH": "字串組合", "TEXT_JOIN_TOOLTIP": "通過連接任意數量的項目來建立一串文字。", - "TEXT_CREATE_JOIN_TITLE_JOIN": "加入", + "TEXT_CREATE_JOIN_TITLE_JOIN": "串連", "TEXT_CREATE_JOIN_TOOLTIP": "添加、刪除或重新排列各部份以重新配置這個文字區塊。", "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "添加一個項目到字串中。", "TEXT_APPEND_TITLE": "至 %1 套用文字 %2", @@ -268,6 +269,7 @@ "LISTS_INDEX_OF_FIRST": "從 最前面 索引項目", "LISTS_INDEX_OF_LAST": "從 最後面 索引項目", "LISTS_INDEX_OF_TOOLTIP": "在清單中檢索是否有包含項目,如果有,返回從頭/倒數算起的索引值。如果沒有則返回 %1。", + "LISTS_GET_INDEX_HELPURL": "https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list", "LISTS_GET_INDEX_GET": "取得", "LISTS_GET_INDEX_GET_REMOVE": "取得並移除", "LISTS_GET_INDEX_REMOVE": "移除", @@ -332,6 +334,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "做些什麼", "PROCEDURES_BEFORE_PARAMS": "與:", "PROCEDURES_CALL_BEFORE_PARAMS": "與:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "無法執行使用者定義的函式「%1」,因為定義區塊已停用。", "PROCEDURES_DEFNORETURN_TOOLTIP": "創建一個無回傳值的函式。", "PROCEDURES_DEFNORETURN_COMMENT": "描述此函式...", "PROCEDURES_DEFRETURN_HELPURL": "https://zh.wikipedia.org/wiki/子程式", diff --git a/msg/messages.js b/msg/messages.js index a313b269a79..6b9d663a68b 100644 --- a/msg/messages.js +++ b/msg/messages.js @@ -1154,9 +1154,9 @@ Blockly.Msg.LISTS_ISEMPTY_TOOLTIP = 'Returns true if the list is empty.'; Blockly.Msg.LISTS_INLIST = 'in list'; /** @type {string} */ -/// {{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list -/// https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list]. -Blockly.Msg.LISTS_INDEX_OF_HELPURL = 'https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list'; +/// {{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list +/// https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list]. +Blockly.Msg.LISTS_INDEX_OF_HELPURL = 'https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list'; /** @type {string} */ Blockly.Msg.LISTS_INDEX_OF_INPUT_IN_LIST = Blockly.Msg.LISTS_INLIST; /** @type {string} */ @@ -1176,7 +1176,9 @@ Blockly.Msg.LISTS_INDEX_OF_LAST = 'find last occurrence of item'; Blockly.Msg.LISTS_INDEX_OF_TOOLTIP = 'Returns the index of the first/last occurrence of the item in the list. Returns %1 if item is not found.'; /** @type {string} */ -Blockly.Msg.LISTS_GET_INDEX_HELPURL = Blockly.Msg.LISTS_INDEX_OF_HELPURL; +/// {{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list +/// https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list]. +Blockly.Msg.LISTS_GET_INDEX_HELPURL = 'https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list'; /** @type {string} */ /// dropdown - Indicates that the user wishes to /// [https://github.com/google/blockly/wiki/Lists#getting-a-single-item @@ -1489,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 34325068383..02335eb531d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "blockly", - "version": "10.4.3", + "version": "11.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "10.4.3", + "version": "11.0.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "jsdom": "22.1.0" + "jsdom": "23.0.0" }, "devDependencies": { "@blockly/block-test": "^5.0.0", @@ -19,7 +19,7 @@ "@hyperjump/json-schema": "^1.5.0", "@microsoft/api-documenter": "^7.22.4", "@microsoft/api-extractor": "^7.29.5", - "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/eslint-plugin": "^7.3.1", "async-done": "^2.0.0", "chai": "^4.2.0", "concurrently": "^8.0.1", @@ -48,9 +48,12 @@ "prettier": "3.2.5", "readline-sync": "^1.4.10", "rimraf": "^5.0.0", - "typescript": "^5.0.2", + "typescript": "^5.3.3", "webdriverio": "^8.32.2", "yargs": "^17.2.1" + }, + "engines": { + "node": ">=18" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -75,9 +78,9 @@ } }, "node_modules/@blockly/block-test": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-5.0.4.tgz", - "integrity": "sha512-3u7z9Xd+W1eCcknqVsmUYnDvS2FIzce2IdGuRC1lst2XIQQ59q2wkUqGg4+Z2Arr3hk/TpIlHn2YLrGPkvqkug==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-5.1.0.tgz", + "integrity": "sha512-beqBTJbrrDGECohJo6uczeLAvRxKgMFn9Ew1po6d8PBka/aNwSkT33jHRUAeasHnraBdFBx8pwYh7By2gVZURQ==", "dev": true, "engines": { "node": ">=8.17.0" @@ -87,14 +90,14 @@ } }, "node_modules/@blockly/dev-tools": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-7.1.5.tgz", - "integrity": "sha512-eNpi+yknoR2RXzYUOE4Owp5XiY9Qm/FH+BOGhd5WrDyK3/LGQ4Yp0NOBO1tWN+Kbjxe19k6yhA50kX7YkDad/w==", + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-7.1.7.tgz", + "integrity": "sha512-ar/6A7JyTSzhJ6ojEOzoCxnm4jRTQJeYn87g+iTEJrM9K8tk/Ccn4sZY34T+ULhy8A2MbFkbtmERCLx6HNrAkA==", "dev": true, "dependencies": { - "@blockly/block-test": "^5.0.4", + "@blockly/block-test": "^5.1.0", "@blockly/theme-dark": "^6.0.5", - "@blockly/theme-deuteranopia": "^5.0.5", + "@blockly/theme-deuteranopia": "^5.0.6", "@blockly/theme-highcontrast": "^5.0.5", "@blockly/theme-tritanopia": "^5.0.5", "chai": "^4.2.0", @@ -124,9 +127,9 @@ } }, "node_modules/@blockly/theme-deuteranopia": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-5.0.5.tgz", - "integrity": "sha512-Eqr3JbIch4Uyi3awWXq0vtGy5LL4cknkBH4VjUi73w9Xdiytt1LgoWJD23NBtKcP4M31iDWV+8fdPK4R1tUVcg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-5.0.6.tgz", + "integrity": "sha512-bYQz2TrkbwPxYjZXlQGf6pMEnSBD/If4FtBqAavt/lutwib0awM0NbPWu8RP89z4aRcAlsByYYSzFQMNqYBaiA==", "dev": true, "engines": { "node": ">=8.17.0" @@ -233,9 +236,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -348,13 +351,13 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -375,9 +378,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "node_modules/@hyperjump/json-pointer": { @@ -576,23 +579,24 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.38.5", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.38.5.tgz", - "integrity": "sha512-c/w2zfqBcBJxaCzpJNvFoouWewcYrUOfeu5ZkWCCIXTF9a/gXM85RGevEzlMAIEGM/kssAAZSXRJIZ3Q5vLFow==", + "version": "7.43.0", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.43.0.tgz", + "integrity": "sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==", "dev": true, "dependencies": { - "@microsoft/api-extractor-model": "7.28.3", + "@microsoft/api-extractor-model": "7.28.13", "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.62.0", - "@rushstack/rig-package": "0.5.1", - "@rushstack/ts-command-line": "4.17.1", - "colors": "~1.2.1", + "@rushstack/node-core-library": "4.0.2", + "@rushstack/rig-package": "0.5.2", + "@rushstack/terminal": "0.10.0", + "@rushstack/ts-command-line": "4.19.1", "lodash": "~4.17.15", + "minimatch": "~3.0.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", - "typescript": "~5.0.4" + "typescript": "5.4.2" }, "bin": { "api-extractor": "bin/api-extractor" @@ -609,6 +613,72 @@ "@rushstack/node-core-library": "3.62.0" } }, + "node_modules/@microsoft/api-extractor/node_modules/@microsoft/api-extractor-model": { + "version": "7.28.13", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.13.tgz", + "integrity": "sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "4.0.2" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/@rushstack/node-core-library": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz", + "integrity": "sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==", + "dev": true, + "dependencies": { + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@microsoft/api-extractor/node_modules/@rushstack/ts-command-line": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.19.1.tgz", + "integrity": "sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==", + "dev": true, + "dependencies": { + "@rushstack/terminal": "0.10.0", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@microsoft/api-extractor/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -619,16 +689,16 @@ } }, "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/@microsoft/tsdoc": { @@ -792,15 +862,70 @@ } }, "node_modules/@rushstack/rig-package": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.1.tgz", - "integrity": "sha512-pXRYSe29TjRw7rqxD4WS3HN/sRSbfr+tJs4a9uuaSIBAITbUggygdhuG0VrO0EO+QqH91GhYMN4S6KRtOEmGVA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.2.tgz", + "integrity": "sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==", "dev": true, "dependencies": { "resolve": "~1.22.1", "strip-json-comments": "~3.1.1" } }, + "node_modules/@rushstack/terminal": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.10.0.tgz", + "integrity": "sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==", + "dev": true, + "dependencies": { + "@rushstack/node-core-library": "4.0.2", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/terminal/node_modules/@rushstack/node-core-library": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz", + "integrity": "sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==", + "dev": true, + "dependencies": { + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/terminal/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/@rushstack/ts-command-line": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.17.1.tgz", @@ -885,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" } @@ -941,9 +1068,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/vinyl": { @@ -982,16 +1109,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.0.tgz", - "integrity": "sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz", + "integrity": "sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.19.0", - "@typescript-eslint/type-utils": "6.19.0", - "@typescript-eslint/utils": "6.19.0", - "@typescript-eslint/visitor-keys": "6.19.0", + "@typescript-eslint/scope-manager": "7.3.1", + "@typescript-eslint/type-utils": "7.3.1", + "@typescript-eslint/utils": "7.3.1", + "@typescript-eslint/visitor-keys": "7.3.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1000,15 +1127,15 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -1016,75 +1143,28 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.0.tgz", - "integrity": "sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.0", - "@typescript-eslint/visitor-keys": "6.19.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.0.tgz", - "integrity": "sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz", - "integrity": "sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.1.0.tgz", - "integrity": "sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.3.1.tgz", + "integrity": "sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.1.0", - "@typescript-eslint/types": "6.1.0", - "@typescript-eslint/typescript-estree": "6.1.0", - "@typescript-eslint/visitor-keys": "6.1.0", + "@typescript-eslint/scope-manager": "7.3.1", + "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/typescript-estree": "7.3.1", + "@typescript-eslint/visitor-keys": "7.3.1", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -1093,17 +1173,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.1.0.tgz", - "integrity": "sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.3.1.tgz", + "integrity": "sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==", "dev": true, - "peer": true, "dependencies": { - "@typescript-eslint/types": "6.1.0", - "@typescript-eslint/visitor-keys": "6.1.0" + "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/visitor-keys": "7.3.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1111,25 +1190,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.0.tgz", - "integrity": "sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.3.1.tgz", + "integrity": "sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.19.0", - "@typescript-eslint/utils": "6.19.0", + "@typescript-eslint/typescript-estree": "7.3.1", + "@typescript-eslint/utils": "7.3.1", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -1137,27 +1216,27 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.0.tgz", - "integrity": "sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==", + "node_modules/@typescript-eslint/types": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.3.1.tgz", + "integrity": "sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.0.tgz", - "integrity": "sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.3.1.tgz", + "integrity": "sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.0", - "@typescript-eslint/visitor-keys": "6.19.0", + "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/visitor-keys": "7.3.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1166,7 +1245,7 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1178,24 +1257,7 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz", - "integrity": "sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", @@ -1204,7 +1266,7 @@ "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", @@ -1219,184 +1281,42 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/types": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.1.0.tgz", - "integrity": "sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ==", - "dev": true, - "peer": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.1.0.tgz", - "integrity": "sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg==", - "dev": true, - "peer": true, - "dependencies": { - "@typescript-eslint/types": "6.1.0", - "@typescript-eslint/visitor-keys": "6.1.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/utils": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.0.tgz", - "integrity": "sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.3.1.tgz", + "integrity": "sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.19.0", - "@typescript-eslint/types": "6.19.0", - "@typescript-eslint/typescript-estree": "6.19.0", + "@typescript-eslint/scope-manager": "7.3.1", + "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/typescript-estree": "7.3.1", "semver": "^7.5.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.0.tgz", - "integrity": "sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.0", - "@typescript-eslint/visitor-keys": "6.19.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.0.tgz", - "integrity": "sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.0.tgz", - "integrity": "sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.0", - "@typescript-eslint/visitor-keys": "6.19.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz", - "integrity": "sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "eslint": "^8.56.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.1.0.tgz", - "integrity": "sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.3.1.tgz", + "integrity": "sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==", "dev": true, - "peer": true, "dependencies": { - "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/types": "7.3.1", "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1568,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", @@ -1655,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", @@ -1679,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": { @@ -2325,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", @@ -3139,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", @@ -3149,19 +3231,54 @@ "type": "^1.0.1" } }, - "node_modules/dat.gui": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.7.tgz", - "integrity": "sha512-sRl/28gF/XRC5ywC9I4zriATTsQcpSsRG7seXCPnTkK8/EQMIbCu5NPMpICLGxX9ZEUvcXR3ArLYCtgreFoMDw==", - "dev": true - }, - "node_modules/data-uri-to-buffer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-5.0.1.tgz", - "integrity": "sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==", - "dev": true, + "node_modules/dat.gui": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.7.tgz", + "integrity": "sha512-sRl/28gF/XRC5ywC9I4zriATTsQcpSsRG7seXCPnTkK8/EQMIbCu5NPMpICLGxX9ZEUvcXR3ArLYCtgreFoMDw==", + "dev": true + }, + "node_modules/data-uri-to-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-5.0.1.tgz", + "integrity": "sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==", + "dev": true, + "engines": { + "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": ">= 14" + "node": ">=18" } }, "node_modules/date-fns": { @@ -3431,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" }, @@ -3746,16 +3866,16 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -4259,9 +4379,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -4803,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", @@ -6307,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" }, @@ -6335,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": { @@ -6388,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": { @@ -6513,9 +6632,9 @@ } }, "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", "dev": true }, "node_modules/is-absolute": { @@ -6933,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": { @@ -6973,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==", + "node_modules/jsdom/node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dependencies": { - "rrweb-cssom": "^0.6.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, - "node_modules/jsdom/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==", + "node_modules/jsdom/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.0" + "punycode": "^2.3.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==", + "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": { - "punycode": "^2.3.0" + "iconv-lite": "0.6.3" }, "engines": { - "node": ">=14" + "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": { @@ -8149,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", @@ -8493,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", @@ -9033,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", @@ -9123,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" } @@ -10111,22 +10170,10 @@ "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.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", "dev": true }, "node_modules/source-map": { @@ -10822,12 +10869,12 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", - "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" @@ -10885,9 +10932,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -10953,9 +11000,9 @@ "dev": true }, "node_modules/undici": { - "version": "5.26.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.3.tgz", - "integrity": "sha512-H7n2zmKEWgOllKkIUkLvFmsJQj062lSm3uA4EYApG8gLuiOM0/go9bIoC3HVaSnfg4xunowDE2i9p8drkXuvDw==", + "version": "5.28.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", + "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", "dev": true, "dependencies": { "@fastify/busboy": "^2.0.0" @@ -11333,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": { @@ -11482,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" }, @@ -11489,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": { @@ -11596,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" }, @@ -11613,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 a7ac581f02d..a0b50d5c67c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "10.4.3", + "version": "11.0.0", "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": { @@ -71,7 +111,7 @@ "@hyperjump/json-schema": "^1.5.0", "@microsoft/api-documenter": "^7.22.4", "@microsoft/api-extractor": "^7.29.5", - "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/eslint-plugin": "^7.3.1", "async-done": "^2.0.0", "chai": "^4.2.0", "concurrently": "^8.0.1", @@ -100,11 +140,14 @@ "prettier": "3.2.5", "readline-sync": "^1.4.10", "rimraf": "^5.0.0", - "typescript": "^5.0.2", + "typescript": "^5.3.3", "webdriverio": "^8.32.2", "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 8775f69189c..d7b118f325c 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 e1756d96e87..90cd571099d 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 46d91ec5f74..48dfd5b8436 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', - }])) - .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'; - */ -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')) - .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', + amd: 'blockly/core', + cjs: 'blockly/core', },{ - name: 'BlocklyPHP', - cjs: './php', + name: 'en', + amd: 'blockly/msg/en', + cjs: 'blockly/msg/en', + global: 'Blockly.Msg', },{ - name: 'BlocklyLua', - cjs: './lua', - }, { - name: 'BlocklyDart', - cjs: './dart', + name: 'blocks', + amd: 'blockly/blocks', + cjs: 'blockly/blocks', + global: 'Blockly.Blocks', }])) - .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. + * 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 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)) +function packageCoreNode() { + return gulp.src('scripts/package/core-node.js') .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/gulpfiles/release_tasks.js b/scripts/gulpfiles/release_tasks.js index 5a0eb9f0e7e..f2545c7b92b 100644 --- a/scripts/gulpfiles/release_tasks.js +++ b/scripts/gulpfiles/release_tasks.js @@ -131,7 +131,7 @@ function updateBetaVersion(done) { const latestBetaVersion = execSync('npm show blockly version --tag beta').toString().trim(); while (!isValid) { newVersion = readlineSync.question(`What is the new beta version? (latest beta version: ${latestBetaVersion})`); - const existsOnNpm = blocklyVersions.indexOf(newVersion) > -1; + const existsOnNpm = blocklyVersions.includes(newVersion); const isFormatted = newVersion.search(re) > -1; if (!existsOnNpm && isFormatted) { isValid = true; diff --git a/scripts/i18n/create_messages.py b/scripts/i18n/create_messages.py index d0d739734b0..3d22a2342da 100755 --- a/scripts/i18n/create_messages.py +++ b/scripts/i18n/create_messages.py @@ -37,12 +37,13 @@ def string_is_ascii(s): def load_constants(filename): """Read in constants file, which must be output in every language.""" constant_defs = read_json_file(filename) + if '#' in constant_defs: # Delete any comment. + del constant_defs['#'] constants_text = '\n' for key in constant_defs: value = constant_defs[key] value = value.replace('"', '\\"') - constants_text += u'\nBlockly.Msg["{0}"] = \"{1}\";'.format( - key, value) + constants_text += u'\nBlockly.Msg["{0}"] = \"{1}\";'.format(key, value) return constants_text def main(): @@ -86,6 +87,8 @@ def main(): # Read in synonyms file, which must be output in every language. synonym_defs = read_json_file(os.path.join( os.curdir, args.source_synonym_file)) + if '#' in synonym_defs: # Delete any comment. + del synonym_defs['#'] # synonym_defs is also being sorted to ensure the same order is kept synonym_text = '\n'.join([u'Blockly.Msg["{0}"] = Blockly.Msg["{1}"];' diff --git a/scripts/i18n/js_to_json.py b/scripts/i18n/js_to_json.py index c070f3408f6..18daf1853f7 100755 --- a/scripts/i18n/js_to_json.py +++ b/scripts/i18n/js_to_json.py @@ -81,7 +81,7 @@ def main(): for line in infile: if line.startswith('///'): if description: - description = description + ' ' + line[3:].strip() + description += ' ' + line[3:].strip() else: description = line[3:].strip() else: @@ -114,29 +114,22 @@ def main(): write_files(args.author, args.lang, args.output_dir, results, False) # Create synonyms.json. - synonyms_sorted = sort_dict(synonyms) synonym_file_name = os.path.join(os.curdir, args.output_dir, 'synonyms.json') + synonyms['#'] = 'Automatically generated, do not edit this file!' with open(synonym_file_name, 'w') as outfile: - json.dump(synonyms_sorted, outfile) + json.dump(synonyms, outfile, indent=2, sort_keys=True) if not args.quiet: print("Wrote {0} synonym pairs to {1}.".format( - len(synonyms_sorted), synonym_file_name)) + len(synonyms) - 1, synonym_file_name)) # Create constants.json - constants_sorted = sort_dict(constants) constants_file_name = os.path.join(os.curdir, args.output_dir, 'constants.json') + constants['#'] = 'Automatically generated, do not edit this file!' with open(constants_file_name, 'w') as outfile: - json.dump(constants_sorted, outfile) + json.dump(constants, outfile, indent=2, sort_keys=True) if not args.quiet: print("Wrote {0} constant pairs to {1}.".format( - len(constants_sorted), synonym_file_name)) - -def sort_dict(unsorted_dict): - # Sort the dictionary (thereby enabling better diffing of changes). - myKeys = list(unsorted_dict.keys()) - myKeys.sort() - sorted_dict = {i: unsorted_dict[i] for i in myKeys} - return sorted_dict + len(constants) - 1, synonym_file_name)) if __name__ == '__main__': main() diff --git a/scripts/migration/renamings.json5 b/scripts/migration/renamings.json5 index bdbb2a132f2..9c51b2d4a3f 100644 --- a/scripts/migration/renamings.json5 +++ b/scripts/migration/renamings.json5 @@ -45,7 +45,7 @@ // The name that the export had before this version. // All of the properties on this object are optional. 'oldExportName': { - // The new module that the export is in in this version. If + // The new module that the export is in this version. If // this is not provided, the newModule is assumed to be the // parent module's newPath. newModule: 'new.module.name', diff --git a/scripts/package/blockly.js b/scripts/package/blockly.js deleted file mode 100644 index 77021b3d63c..00000000000 --- 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 66ae806bffa..00000000000 --- 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 b7adcc92adf..00000000000 --- 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 36d0c325bf2..00000000000 --- 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 00000000000..22c3c5bcddf --- /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 d59060bded7..00000000000 --- 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 0264fd446b5..8eb70b4265d 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 e4adba09fd2..00000000000 --- 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 ca37b44f995..00000000000 --- 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 b789e1f3126..00000000000 --- 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 33c322a98e6..00000000000 --- 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 a6ed3fa6d3b..00000000000 --- 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 0bb64c06f7d..00000000000 --- 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 e9887243eba..00000000000 --- 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 00000000000..b6adfebcd20 --- /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 05a9dee4856..00000000000 --- 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 a4788658839..f565ecae131 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 f8ac5651a51..9fa4f1d9328 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. @@ -513,7 +513,7 @@ function mathModes(values) { } for (var j = 0; j < counts.length; j++) { if (counts[j][1] === maxCount) { - modes.push(counts[j][0]); + modes.push(counts[j][0]); } } return modes; @@ -527,7 +527,7 @@ function mathStandardDeviation(numbers) { for (var j = 0; j < n; j++) { variance += Math.pow(numbers[j] - mean, 2); } - variance = variance / n; + variance /= n; return Math.sqrt(variance); } @@ -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 085a76070be..63dbe0af4a9 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 4a16b6f5cc3..d497f1a70c7 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 @@ function recurse($n) { test_count_text(); test_text_reverse(); test_replace(); -test_multiline(); print(unittest_report()); $unittestResults = null; @@ -1672,15 +1590,6 @@ function recurse($n) { 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 e4375dbbcec..bafa0983b5a 100644 --- a/tests/generators/golden/generated.py +++ b/tests/generators/golden/generated.py @@ -27,7 +27,6 @@ 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 try_float(s): # 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 @@ def recurse(n): test_count_text() test_text_reverse() test_replace() -test_multiline() print(unittest_report()) unittestResults = None @@ -1443,15 +1386,6 @@ def recurse(n): 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 cb30c096691..9ec1db7731d 100644 --- a/tests/generators/index.html +++ b/tests/generators/index.html @@ -321,7 +321,6 @@ - @@ -337,12 +336,6 @@ - - - - - - @@ -364,7 +357,6 @@

Blockly Generator Tests

Math
Text
Lists
- Colour
Variables
Functions
diff --git a/tests/generators/text.xml b/tests/generators/text.xml index d10593735c6..61e961aefd6 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/generators/unittest_dart.js b/tests/generators/unittest_dart.js index 312a8fcc28b..89f3553899f 100644 --- a/tests/generators/unittest_dart.js +++ b/tests/generators/unittest_dart.js @@ -154,7 +154,7 @@ dartGenerator.forBlock['unittest_adjustindex'] = function(block) { return [Number(index) + 1, dartGenerator.ORDER_ATOMIC]; } else { // If the index is dynamic, adjust it in code. - index = index + ' + 1'; + index += ' + 1'; } } else if (Blockly.utils.string.isNumber(index)) { return [index, dartGenerator.ORDER_ATOMIC]; diff --git a/tests/generators/unittest_javascript.js b/tests/generators/unittest_javascript.js index 279244eb22e..77953a37a7f 100644 --- a/tests/generators/unittest_javascript.js +++ b/tests/generators/unittest_javascript.js @@ -158,7 +158,7 @@ javascriptGenerator.forBlock['unittest_adjustindex'] = function(block) { return [Number(index) + 1, javascriptGenerator.ORDER_ATOMIC]; } else { // If the index is dynamic, adjust it in code. - index = index + ' + 1'; + index += ' + 1'; } } else if (Blockly.utils.string.isNumber(index)) { return [index, javascriptGenerator.ORDER_ATOMIC]; diff --git a/tests/generators/unittest_php.js b/tests/generators/unittest_php.js index 408960d8235..aedd7e6a186 100644 --- a/tests/generators/unittest_php.js +++ b/tests/generators/unittest_php.js @@ -145,7 +145,7 @@ phpGenerator.forBlock['unittest_adjustindex'] = function(block) { return [Number(index) + 1, phpGenerator.ORDER_ATOMIC]; } else { // If the index is dynamic, adjust it in code. - index = index + ' + 1'; + index += ' + 1'; } } else if (Blockly.utils.string.isNumber(index)) { return [index, phpGenerator.ORDER_ATOMIC]; diff --git a/tests/generators/unittest_python.js b/tests/generators/unittest_python.js index ba29015f9a0..49f8c9100ff 100644 --- a/tests/generators/unittest_python.js +++ b/tests/generators/unittest_python.js @@ -129,7 +129,7 @@ pythonGenerator.forBlock['unittest_adjustindex'] = function(block) { return [Number(index) + 1, pythonGenerator.ORDER_ATOMIC]; } else { // If the index is dynamic, adjust it in code. - index = index + ' + 1'; + index += ' + 1'; } } else if (Blockly.utils.string.isNumber(index)) { return [index, pythonGenerator.ORDER_ATOMIC]; diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index f5b2e6a82e0..e158391fc12 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 00000000000..3bbfdac1084 --- /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 6173179cd0c..109d3b2d4eb 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 f134b1d7795..fb0c4188275 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 494c584e787..843453278e4 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 6f19aa7f057..452f07493cf 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 00000000000..6650848e51f --- /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 7c68d0858b1..c2355f87462 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 00000000000..86b36b07588 --- /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 d140eff8526..57c246f1f1e 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 fc4b7701fa3..e0a8a98db33 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 a0b0b5eabf3..420bdbb52de 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 e59cb4911a1..75688c6547e 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 62afd8d2761..00000000000 --- a/tests/mocha/field_angle_test.js +++ /dev/null @@ -1,389 +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.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 abdf6823722..00000000000 --- 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/field_number_test.js b/tests/mocha/field_number_test.js index 4dcc930da5f..c6737668d05 100644 --- a/tests/mocha/field_number_test.js +++ b/tests/mocha/field_number_test.js @@ -261,6 +261,7 @@ suite('Number Fields', function () { suite('Validators', function () { setup(function () { this.field = new Blockly.FieldNumber(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'); @@ -276,7 +277,7 @@ suite('Number Fields', function () { return null; }, value: 2, - expectedValue: '1', + expectedValue: 1, }, { title: 'Force End with 6 Validator', diff --git a/tests/mocha/field_registry_test.js b/tests/mocha/field_registry_test.js index f8e4bb9b4cd..aca5487469b 100644 --- a/tests/mocha/field_registry_test.js +++ b/tests/mocha/field_registry_test.js @@ -42,12 +42,10 @@ suite('Field Registry', function () { }, 'Invalid name'); }); test('No fromJson', function () { - const fromJson = CustomFieldType.fromJson; - delete CustomFieldType.fromJson; + class IncorrectField {} chai.assert.throws(function () { - Blockly.fieldRegistry.register('field_custom_test', CustomFieldType); + Blockly.fieldRegistry.register('field_custom_test', IncorrectField); }, 'must have a fromJson function'); - CustomFieldType.fromJson = fromJson; }); test('fromJson not a function', function () { const fromJson = CustomFieldType.fromJson; @@ -97,5 +95,21 @@ suite('Field Registry', function () { chai.assert.isNotNull(field); chai.assert.equal(field.getValue(), 'ok'); }); + test('Did not override fromJson', function () { + // This class will have a fromJson method, so it can be registered + // but it doesn't override the abstract class's method so it throws + class IncorrectField extends Blockly.Field {} + + Blockly.fieldRegistry.register('field_custom_test', IncorrectField); + + const json = { + type: 'field_custom_test', + value: 'ok', + }; + + chai.assert.throws(function () { + Blockly.fieldRegistry.fromJson(json); + }, 'Attempted to instantiate a field from the registry'); + }); }); }); diff --git a/tests/mocha/field_textinput_test.js b/tests/mocha/field_textinput_test.js index 5a8a8247978..f68006e8768 100644 --- a/tests/mocha/field_textinput_test.js +++ b/tests/mocha/field_textinput_test.js @@ -129,6 +129,7 @@ suite('Text Input Fields', function () { suite('Validators', function () { setup(function () { this.field = new Blockly.FieldTextInput('value'); + this.field.valueWhenEditorWasOpened_ = this.field.getValue(); this.field.htmlInput_ = document.createElement('input'); this.field.htmlInput_.setAttribute('data-old-value', 'value'); this.field.htmlInput_.setAttribute('data-untyped-default-value', 'value'); diff --git a/tests/mocha/generator_test.js b/tests/mocha/generator_test.js index 9ac67b27ca3..3a2679dca81 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 4ff27997fe8..3463d8ad83b 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 6c4e5ad0cba..9c7b10cabe2 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 c4ecd7a3905..73baf20f6c7 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 d58e208fbe0..7c8a06db856 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': [], - }; - - 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.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', - ); - }); - }); - - 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, - }; - - this.procedureSerializer.load([jso], this.workspace); - - 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', - ); - }); - - 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'], - }; + 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.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', - ); - }); + chai.assert.isTrue( + spy.calledOnce, + 'Expected the loadState method to be called', + ); }); - 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('load is called for each parameter model', function () { + const state = [ + { + 'id': 'test', + 'parameters': [ + { + 'id': 'test1', + }, + { + 'id': 'test2', + }, + ], + }, + ]; - 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'], - }, - ], - }; + const spy = this.sandbox.spy(MockParameterModel, 'loadState'); - this.procedureSerializer.load([jso], this.workspace); + this.procedureSerializer.load(state, 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 895ff0be276..72a74ad3d6a 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.setDisabledReason(false, 'test reason'); + const jso = Blockly.serialization.blocks.save(block); + assertNoProperty(jso, 'disabledReasons'); + }); + + test('Multiple reasons', function () { const block = this.workspace.newBlock('row_block'); - block.setEnabled(true); + block.setDisabledReason(true, 'test reason 1'); + block.setDisabledReason(true, 'test reason 2'); const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'enabled'); + 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('saving IDs can be disabled', function () { + const comment = new Blockly.comments.WorkspaceComment( + this.workspace, + 'testID', + ); + + const json = Blockly.serialization.workspaceComments.save(comment, { + saveIds: false, + }); + + assertNoProperty(json, 'id'); + }); + }); + + 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('saving coordinates can be enabled', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.moveTo(new Blockly.utils.Coordinate(42, 1337)); + + const json = Blockly.serialization.workspaceComments.save(comment, { + addCoordinates: true, + }); + + assertProperty(json, 'x', 42); + assertProperty(json, 'y', 1337); + }); + }); + + suite('Text', function () { + test('the empty string is not saved', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setText(''); + + const json = Blockly.serialization.workspaceComments.save(comment); + + assertNoProperty(json, 'text'); }); - 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('text is saved', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setText('test text'); + + 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); - 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); + + 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('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('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('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('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); - 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']); + 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('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('Deletable', function () { + test('deletable is not saved if true', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setDeletable(true); - 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, 'deletable'); }); - 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('deletable is saved if false', function () { + const comment = new Blockly.comments.WorkspaceComment(this.workspace); + comment.setDeletable(false); - 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, 'deletable', false); }); }); }); diff --git a/tests/mocha/mutator_test.js b/tests/mocha/mutator_test.js index 609dc03a28d..b4c6930fa36 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 00000000000..3bef493f3d2 --- /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/registry_test.js b/tests/mocha/registry_test.js index fd37c6e6963..b562606f479 100644 --- a/tests/mocha/registry_test.js +++ b/tests/mocha/registry_test.js @@ -47,6 +47,15 @@ suite('Registry', function () { test('Overwrite a Key', function () { Blockly.registry.register('test', 'test_name', TestClass); chai.assert.throws(function () { + // Registers a different object under the same name + Blockly.registry.register('test', 'test_name', {}); + }, 'already registered'); + }); + + test('Register a Duplicate Item', function () { + Blockly.registry.register('test', 'test_name', TestClass); + chai.assert.doesNotThrow(function () { + // Registering the same object under the same name is allowed Blockly.registry.register('test', 'test_name', TestClass); }, 'already registered'); }); diff --git a/tests/mocha/render_management_test.js b/tests/mocha/render_management_test.js index 4a69d2bab99..7852a5b0ca3 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 bd9a4734417..b10f48df515 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 e4ddc0e3f1c..4717b38c8b6 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/user_input.js b/tests/mocha/test_helpers/user_input.js index 6606a665248..a5902a7db02 100644 --- a/tests/mocha/test_helpers/user_input.js +++ b/tests/mocha/test_helpers/user_input.js @@ -41,10 +41,10 @@ export function createKeyDownEvent(keyCode, modifiers) { keyCode: keyCode, }; if (modifiers && modifiers.length > 0) { - event.altKey = modifiers.indexOf(KeyCodes.ALT) > -1; - event.ctrlKey = modifiers.indexOf(KeyCodes.CTRL) > -1; - event.metaKey = modifiers.indexOf(KeyCodes.META) > -1; - event.shiftKey = modifiers.indexOf(KeyCodes.SHIFT) > -1; + event.altKey = modifiers.includes(KeyCodes.ALT); + event.ctrlKey = modifiers.includes(KeyCodes.CTRL); + event.metaKey = modifiers.includes(KeyCodes.META); + event.shiftKey = modifiers.includes(KeyCodes.SHIFT); } return new KeyboardEvent('keydown', event); } diff --git a/tests/mocha/test_helpers/workspace.js b/tests/mocha/test_helpers/workspace.js index 7654feba29d..6da113f8b4c 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 f2126dea293..aa42cfbe2ac 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,141 @@ 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.clock = sharedTestSetup.call(this, {fireEventsNow: false}).clock; + 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); - }); - test('After clear empty workspace', function () { - this.workspace.clear(); - chai.assert.equal(this.workspace.getTopComments(true).length, 0); - }); + this.clock.runAll(); - test('After clear non-empty workspace', function () { - new Blockly.WorkspaceComment( - this.workspace, - 'comment text', - 0, - 0, - 'comment id', + assertEventFired( + spy, + Blockly.Events.CommentCreate, + {commentId: this.renderedComment.id}, + this.workspace.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( + 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', ); - 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.dispose(); - 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); - }); + this.clock.runAll(); - 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', + assertEventFired( + spy, + Blockly.Events.CommentDelete, + {commentId: this.renderedComment.id}, + this.workspace.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( + 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(false).length, 0); - chai.assert.isFalse(this.workspace.commentDB.has('comment id')); - }); - }); + 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); - }); - - test('Null id', function () { - chai.assert.isNull(this.workspace.getCommentById(null)); - }); - - test('Non-existent id', function () { - chai.assert.isNull(this.workspace.getCommentById('badId')); - }); + this.renderedComment.moveTo(new Blockly.utils.Coordinate(42, 42)); - 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)); - }); - }); + this.clock.runAll(); - suite('dispose', function () { - test('Called twice', function () { - const comment = new Blockly.WorkspaceComment( - this.workspace, - 'comment text', - 0, - 0, - 'comment id', + assertEventFired( + spy, + Blockly.Events.CommentMove, + { + commentId: this.renderedComment.id, + oldCoordinate_: {x: 0, y: 0}, + newCoordinate_: {x: 42, y: 42}, + }, + this.workspace.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( + test('change events are fired when a comments text is edited', function () { + this.renderedComment = new Blockly.comments.RenderedWorkspaceComment( this.workspace, - 'comment text', - 10, - 20, - 'comment id', ); - }); + const spy = createChangeListenerSpy(this.workspace); - test('Initial values', function () { - chai.assert.equal(this.comment.getWidth(), 20, 'Width'); - chai.assert.equal(this.comment.getHeight(), 10, 'Height'); - }); + this.renderedComment.setText('test text'); - 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'); - }); + this.clock.runAll(); - 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'); + assertEventFired( + spy, + Blockly.Events.CommentChange, + { + commentId: this.renderedComment.id, + oldContents_: '', + newContents_: 'test text', + }, + this.workspace.id, + ); }); - }); - suite('XY position', function () { - setup(function () { - this.comment = new Blockly.WorkspaceComment( + test('collapse events are fired when a comment is collapsed', function () { + this.renderedComment = new Blockly.comments.RenderedWorkspaceComment( this.workspace, - 'comment text', - 10, - 20, - 'comment id', ); - }); + const spy = createChangeListenerSpy(this.workspace); - 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'); - }); + this.renderedComment.setCollapsed(true); - 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'); - }); - }); + this.clock.runAll(); - suite('Content', function () { - setup(function () { - this.comment = new Blockly.WorkspaceComment( - this.workspace, - 'comment text', - 0, - 0, - 'comment id', + assertEventFired( + spy, + Blockly.Events.CommentCollapse, + { + commentId: this.renderedComment.id, + newCollapsed: true, + }, + this.workspace.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('collapse events are fired when a comment is uncollapsed', function () { + this.renderedComment = new Blockly.comments.RenderedWorkspaceComment( + this.workspace, ); - }); + this.renderedComment.setCollapsed(true); + const spy = createChangeListenerSpy(this.workspace); - 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', - ); - }); + this.renderedComment.setCollapsed(false); + + this.clock.runAll(); - 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 cea58cdf920..98ca14d5754 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 385b0f73665..7bd16afd050 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 f929de8d8b1..ff59127e6f7 100644 --- a/tests/multi_playground.html +++ b/tests/multi_playground.html @@ -342,7 +342,6 @@

Blockly Multi Playground

- @@ -469,44 +468,6 @@

Blockly Multi Playground

- - - - - - - 100 - - - - - 50 - - - - - 0 - - - - - - - #ff0000 - - - - - #3333ff - - - - - 0.5 - - - - toCode('Dart')); document - .getElementById('airstrike') - .addEventListener('click', () => airstrike(100)); + .getElementById('scatter') + .addEventListener('click', () => scatter(100)); document .getElementById('spaghetti-xml') .addEventListener('click', () => spaghetti(8)); @@ -567,7 +566,7 @@

Blockly Playground

Stress test:   - +

@@ -765,7 +764,6 @@

Blockly Playground

- @@ -918,44 +916,6 @@

Blockly Playground

- - - - - - - 100 - - - - - 50 - - - - - 0 - - - - - - - #ff0000 - - - - - #3333ff - - - - - 0.5 - - - - Blockly Playground - diff --git a/tests/playgrounds/advanced_playground.html b/tests/playgrounds/advanced_playground.html index 515ff557f2a..1fcbbd812a9 100644 --- a/tests/playgrounds/advanced_playground.html +++ b/tests/playgrounds/advanced_playground.html @@ -26,7 +26,6 @@ function createWorkspace(blocklyDiv, options) { var workspace = Blockly.inject(blocklyDiv, options); - workspace.configureContextMenu = configureContextMenu.bind(workspace); return workspace; } @@ -101,6 +100,8 @@ }, }; + Blockly.ContextMenuItems.registerCommentOptions(); + createPlayground( document.getElementById('root'), createWorkspace, @@ -125,23 +126,6 @@ document.body.style.backgroundColor = '#d6d6ff'; // Familliar lilac. } } - - function configureContextMenu(menuOptions, e) { - var workspace = this; - var screenshotOption = { - text: 'Download Screenshot', - enabled: workspace.getTopBlocks().length, - callback: function () { - downloadScreenshot(workspace); - }, - }; - menuOptions.push(screenshotOption); - - // Adds a default-sized workspace comment to the workspace. - menuOptions.push( - Blockly.ContextMenu.workspaceCommentOption(workspace, e), - ); - } start(); diff --git a/tests/playgrounds/screenshot.js b/tests/playgrounds/screenshot.js index af20f9d52c5..c443755a53e 100644 --- a/tests/playgrounds/screenshot.js +++ b/tests/playgrounds/screenshot.js @@ -89,8 +89,7 @@ function workspaceToSvg_(workspace, callback, customCss) { const css = [].slice .call(document.head.querySelectorAll('style')) .filter( - (el) => - /\.blocklySvg/.test(el.innerText) || el.id.indexOf('blockly-') === 0, + (el) => /\.blocklySvg/.test(el.innerText) || el.id.startsWith('blockly-'), ) .map((el) => el.innerText) .join('\n'); diff --git a/tests/scripts/check_metadata.sh b/tests/scripts/check_metadata.sh index f8d7b74579c..5c4eb5dcc98 100755 --- a/tests/scripts/check_metadata.sh +++ b/tests/scripts/check_metadata.sh @@ -38,7 +38,8 @@ readonly RELEASE_DIR='dist' # Q3 2023 10.1.3 898859 # Q4 2023 10.2.2 903535 # Q1 2024 10.3.1 914366 -readonly BLOCKLY_SIZE_EXPECTED=914366 +# Q2 2024 11.0.0 905365 +readonly BLOCKLY_SIZE_EXPECTED=905365 # Size of blocks_compressed.js # Q2 2019 2.20190722.0 75618 @@ -62,7 +63,8 @@ readonly BLOCKLY_SIZE_EXPECTED=914366 # Q3 2023 10.1.3 90150 # Q4 2023 10.2.2 90269 # Q1 2024 10.3.1 90269 -readonly BLOCKS_SIZE_EXPECTED=90269 +# Q2 2024 11.0.0 88376 +readonly BLOCKS_SIZE_EXPECTED=88376 # Size of blockly_compressed.js.gz # Q2 2019 2.20190722.0 180925 @@ -87,7 +89,8 @@ readonly BLOCKS_SIZE_EXPECTED=90269 # Q3 2023 10.1.3 180553 # Q4 2023 10.2.2 181474 # Q1 2024 10.3.1 184237 -readonly BLOCKLY_GZ_SIZE_EXPECTED=184237 +# Q2 2024 11.0.0 182249 +readonly BLOCKLY_GZ_SIZE_EXPECTED=182249 # Size of blocks_compressed.js.gz # Q2 2019 2.20190722.0 14552 @@ -111,7 +114,8 @@ readonly BLOCKLY_GZ_SIZE_EXPECTED=184237 # Q3 2023 10.1.3 16508 # Q4 2023 10.2.2 16442 # Q1 2024 10.3.1 16533 -readonly BLOCKS_GZ_SIZE_EXPECTED=16533 +# Q2 2024 11.0.0 15815 +readonly BLOCKS_GZ_SIZE_EXPECTED=15815 # ANSI colors readonly BOLD_GREEN='\033[1;32m' diff --git a/tests/scripts/load.mjs b/tests/scripts/load.mjs index 728a4fd2c75..f2238cf1c28 100644 --- a/tests/scripts/load.mjs +++ b/tests/scripts/load.mjs @@ -68,7 +68,7 @@ export const COMPRESSED = compressed(); * * When loading in uncompressed mode, if scriptExports is a simple * variable name (e.g. 'Blockly') then globalThis[scriptExports] will - * be set to the the chunk's Module object. This attempts to provide + * be set to the chunk's Module object. This attempts to provide * backward compatibility with loading the compressed chunk as a * script, where this is done by the compressed chunk's UMD wrapper. * The compatibility is not complete, however: since Module objects diff --git a/tests/typescript/src/generators.ts b/tests/typescript/src/generators.ts index a87d70ee334..fd79a3a00f6 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 00000000000..ae285e5e82b --- /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 00000000000..716c91459c4 --- /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 00000000000..b030ff7a7fc --- /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 00000000000..cb9241b4374 --- /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 00000000000..aa1c19c38d6 --- /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 00000000000..beb63a612a9 --- /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 bf89e4f06c8..0f6b1e090fc 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 ed1106bbcbe..e5558381d2d 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 752e8521a39..6443fe90b9e 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 5f2d13710bd..b6e6cc1d653 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 8908bc93668..00000000000 --- 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 00000000000..b6e6cc1d653 --- /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 00000000000..b6e6cc1d653 --- /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 7880cc9831f..00000000000 --- 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 f2e0dfc7622..00000000000 --- 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 00000000000..b6e6cc1d653 --- /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 deaf9b89957..96810bc30f0 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 c0a2c284b0a..fd1e3c67793 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';