diff --git a/core/block.ts b/core/block.ts index de8f16680..14b34972e 100644 --- a/core/block.ts +++ b/core/block.ts @@ -1066,11 +1066,12 @@ export class Block implements IASTNodeLocation, IDeletable { * @returns List of variable ids. */ getVars(): string[] { - const vars = []; + const vars: string[] = []; for (let i = 0, input; input = this.inputList[i]; i++) { for (let j = 0, field; field = input.fieldRow[j]; j++) { if (field.referencesVariables()) { - vars.push(field.getValue()); + // NOTE: This only applies to `FieldVariable`, a `Field` + vars.push(field.getValue() as string); } } } diff --git a/core/field.ts b/core/field.ts index cf9148e1c..0bcf99119 100644 --- a/core/field.ts +++ b/core/field.ts @@ -45,12 +45,30 @@ import * as WidgetDiv from './widgetdiv.js'; import type {WorkspaceSvg} from './workspace_svg.js'; import * as Xml from './xml.js'; -export type FieldValidator = (value?: T) => T|null|undefined; +/** + * A function that is called to validate changes to the field's value before + * they are set. + * + * **NOTE:** Validation returns one option between `T`, `null`, and `undefined`. + * + * @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 FieldValidator = (newValue: T) => T|null|undefined; /** * Abstract class for an editable field. * * @alias Blockly.Field + * @typeParam T - The value stored on the field. */ export abstract class Field implements IASTNodeLocationSvg, IASTNodeLocationWithBlock, @@ -76,6 +94,9 @@ export abstract class Field implements IASTNodeLocationSvg, * instead. */ static readonly SKIP_SETUP = new Sentinel(); + static isSentinel(value: T|Sentinel): value is Sentinel { + return value === Field.SKIP_SETUP; + } /** * Name of field. Unique within each block. @@ -207,9 +228,7 @@ export abstract class Field implements IASTNodeLocationSvg, /** The size of the area rendered by the field. */ this.size_ = new Size(0, 0); - if (value === Field.SKIP_SETUP) { - return; - } + if (Field.isSentinel(value)) return; if (opt_config) { this.configure_(opt_config); } @@ -372,7 +391,8 @@ export abstract class Field implements IASTNodeLocationSvg, * @internal */ fromXml(fieldElement: Element) { - this.setValue(fieldElement.textContent); + // Any because gremlins live here. No touchie! + this.setValue(fieldElement.textContent as any); } /** @@ -384,7 +404,8 @@ export abstract class Field implements IASTNodeLocationSvg, * @internal */ toXml(fieldElement: Element): Element { - fieldElement.textContent = this.getValue(); + // Any because gremlins live here. No touchie! + fieldElement.textContent = this.getValue() as any; return fieldElement; } @@ -619,7 +640,7 @@ export abstract class Field implements IASTNodeLocationSvg, * * @returns Validation function, or null. */ - getValidator(): Function|null { + getValidator(): FieldValidator|null { return this.validator_; } @@ -965,41 +986,37 @@ export abstract class Field implements IASTNodeLocationSvg, return; } - let validatedValue = this.doClassValidation_(newValue); - // Class validators might accidentally forget to return, we'll ignore that. - newValue = this.processValidation_(newValue, validatedValue); - if (newValue instanceof Error) { + const classValidation = this.doClassValidation_(newValue); + const classValue = this.processValidation_(newValue, classValidation); + if (classValue instanceof Error) { doLogging && console.log('invalid class validation, return'); return; } - const localValidator = this.getValidator(); - if (localValidator) { - validatedValue = localValidator.call(this, newValue); - // Local validators might accidentally forget to return, we'll ignore - // that. - newValue = this.processValidation_(newValue, validatedValue); - if (newValue instanceof Error) { - doLogging && console.log('invalid local validation, return'); - return; - } + const localValidation = this.getValidator()?.call(this, classValue); + const localValue = this.processValidation_(classValue, localValidation); + if (localValue instanceof Error) { + doLogging && console.log('invalid local validation, return'); + return; } + const source = this.sourceBlock_; if (source && source.disposed) { doLogging && console.log('source disposed, return'); return; } + const oldValue = this.getValue(); - if (oldValue === newValue) { + if (oldValue === localValue) { doLogging && console.log('same, doValueUpdate_, return'); - this.doValueUpdate_(newValue); + this.doValueUpdate_(localValue); return; } - this.doValueUpdate_(newValue); + this.doValueUpdate_(localValue); if (source && eventUtils.isEnabled()) { eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))( - source, 'field', this.name || null, oldValue, newValue)); + source, 'field', this.name || null, oldValue, localValue)); } if (this.isDirty_) { this.forceRerender(); @@ -1015,8 +1032,7 @@ export abstract class Field implements IASTNodeLocationSvg, * @returns New value, or an Error object. */ private processValidation_( - newValue: AnyDuringMigration, - validatedValue: AnyDuringMigration): AnyDuringMigration { + newValue: AnyDuringMigration, validatedValue: T|null|undefined): T|Error { if (validatedValue === null) { this.doValueInvalid_(newValue); if (this.isDirty_) { @@ -1024,10 +1040,7 @@ export abstract class Field implements IASTNodeLocationSvg, } return Error(); } - if (validatedValue !== undefined) { - newValue = validatedValue; - } - return newValue; + return validatedValue === undefined ? newValue as T : validatedValue; } /** @@ -1035,23 +1048,39 @@ export abstract class Field implements IASTNodeLocationSvg, * * @returns Current value. */ - getValue(): AnyDuringMigration { + getValue(): T|null { return this.value_; } /** - * Used to validate a value. Returns input by default. Can be overridden by - * subclasses, see FieldDropdown. + * Validate the changes to a field's value before they are set. See + * **FieldDropdown** for an example of subclass implementation. * - * @param opt_newValue The value to be validated. - * @returns The validated value, same as input by default. + * **NOTE:** Validation returns one option between `T`, `null`, and + * `undefined`. **Field**'s implementation will never return `undefined`, but + * it is valid for a subclass to return `undefined` if the new value is + * compatible with `T`. + * + * @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. */ - protected doClassValidation_(opt_newValue?: AnyDuringMigration): - AnyDuringMigration { - if (opt_newValue === null || opt_newValue === undefined) { + protected doClassValidation_(newValue: T): T|null|undefined; + protected doClassValidation_(newValue?: AnyDuringMigration): T|null; + protected doClassValidation_(newValue?: T|AnyDuringMigration): T|null + |undefined { + if (newValue === null || newValue === undefined) { return null; } - return opt_newValue; + + return newValue as T; } /** @@ -1060,7 +1089,7 @@ export abstract class Field implements IASTNodeLocationSvg, * * @param newValue The value to be saved. */ - protected doValueUpdate_(newValue: AnyDuringMigration) { + protected doValueUpdate_(newValue: T) { this.value_ = newValue; this.isDirty_ = true; } @@ -1086,7 +1115,7 @@ export abstract class Field implements IASTNodeLocationSvg, } const gesture = (this.sourceBlock_.workspace as WorkspaceSvg).getGesture(e); if (gesture) { - gesture.setStartField(this as Field); + gesture.setStartField(this); } } diff --git a/core/field_angle.ts b/core/field_angle.ts index 448147c65..70cce0f4e 100644 --- a/core/field_angle.ts +++ b/core/field_angle.ts @@ -137,9 +137,7 @@ export class FieldAngle extends FieldInput { opt_config?: FieldAngleConfig) { super(Field.SKIP_SETUP); - if (opt_value === Field.SKIP_SETUP) { - return; - } + if (Field.isSentinel(opt_value)) return; if (opt_config) { this.configure_(opt_config); } @@ -404,25 +402,24 @@ export class FieldAngle extends FieldInput { * * @param e Keyboard event. */ - protected override onHtmlInputKeyDown_(e: Event) { + protected override onHtmlInputKeyDown_(e: KeyboardEvent) { super.onHtmlInputKeyDown_(e); const block = this.getSourceBlock(); if (!block) { throw new UnattachedFieldError(); } - const keyboardEvent = e as KeyboardEvent; let multiplier; - if (keyboardEvent.keyCode === KeyCodes.LEFT) { + if (e.keyCode === KeyCodes.LEFT) { // decrement (increment in RTL) multiplier = block.RTL ? 1 : -1; - } else if (keyboardEvent.keyCode === KeyCodes.RIGHT) { + } else if (e.keyCode === KeyCodes.RIGHT) { // increment (decrement in RTL) multiplier = block.RTL ? -1 : 1; - } else if (keyboardEvent.keyCode === KeyCodes.DOWN) { + } else if (e.keyCode === KeyCodes.DOWN) { // decrement multiplier = -1; - } else if (keyboardEvent.keyCode === KeyCodes.UP) { + } else if (e.keyCode === KeyCodes.UP) { // increment multiplier = 1; } diff --git a/core/field_checkbox.ts b/core/field_checkbox.ts index 1007e1884..3b8c08a92 100644 --- a/core/field_checkbox.ts +++ b/core/field_checkbox.ts @@ -20,14 +20,16 @@ import {Field, FieldConfig, FieldValidator} from './field.js'; import * as fieldRegistry from './field_registry.js'; import type {Sentinel} from './utils/sentinel.js'; -export type FieldCheckboxValidator = FieldValidator; +type BoolString = 'TRUE'|'FALSE'; +type CheckboxBool = BoolString|boolean; +export type FieldCheckboxValidator = FieldValidator; /** * Class for a checkbox field. * * @alias Blockly.FieldCheckbox */ -export class FieldCheckbox extends Field { +export class FieldCheckbox extends Field { /** Default character for the checkmark. */ static readonly CHECK_CHAR = '✓'; private checkChar_: string; @@ -42,7 +44,12 @@ export class FieldCheckbox extends Field { * Mouse cursor style when over the hotspot that initiates editability. */ override CURSOR = 'default'; - override value_: AnyDuringMigration; + + /** + * NOTE: The default value is set in `Field`, so maintain that value instead + * of overwriting it here or in the constructor. + */ + override value_: boolean|null = this.value_; /** * @param opt_value The initial value of the field. Should either be 'TRUE', @@ -59,8 +66,7 @@ export class FieldCheckbox extends Field { * for a list of properties this parameter supports. */ constructor( - opt_value?: string|boolean|Sentinel, - opt_validator?: FieldCheckboxValidator, + opt_value?: CheckboxBool|Sentinel, opt_validator?: FieldCheckboxValidator, opt_config?: FieldCheckboxConfig) { super(Field.SKIP_SETUP); @@ -70,9 +76,7 @@ export class FieldCheckbox extends Field { */ this.checkChar_ = FieldCheckbox.CHECK_CHAR; - if (opt_value === Field.SKIP_SETUP) { - return; - } + if (Field.isSentinel(opt_value)) return; if (opt_config) { this.configure_(opt_config); } @@ -153,7 +157,7 @@ export class FieldCheckbox extends Field { * @returns A valid value ('TRUE' or 'FALSE), or null if invalid. */ protected override doClassValidation_(opt_newValue?: AnyDuringMigration): - string|null { + BoolString|null { if (opt_newValue === true || opt_newValue === 'TRUE') { return 'TRUE'; } @@ -169,7 +173,7 @@ export class FieldCheckbox extends Field { * @param newValue The value to be saved. The default validator guarantees * that this is a either 'TRUE' or 'FALSE'. */ - protected override doValueUpdate_(newValue: AnyDuringMigration) { + protected override doValueUpdate_(newValue: BoolString) { this.value_ = this.convertValueToBool_(newValue); // Update visual. if (this.textElement_) { @@ -182,7 +186,7 @@ export class FieldCheckbox extends Field { * * @returns The value of this field. */ - override getValue(): string { + override getValue(): BoolString { return this.value_ ? 'TRUE' : 'FALSE'; } @@ -191,8 +195,8 @@ export class FieldCheckbox extends Field { * * @returns The boolean value of this field. */ - getValueBoolean(): boolean { - return this.value_ as boolean; + getValueBoolean(): boolean|null { + return this.value_; } /** @@ -213,12 +217,9 @@ export class FieldCheckbox extends Field { * @param value The value to convert. * @returns The converted value. */ - private convertValueToBool_(value: AnyDuringMigration): boolean { - if (typeof value === 'string') { - return value === 'TRUE'; - } else { - return !!value; - } + private convertValueToBool_(value: CheckboxBool|null): boolean { + if (typeof value === 'string') return value === 'TRUE'; + return !!value; } /** diff --git a/core/field_colour.ts b/core/field_colour.ts index 729ebe54f..db56acf53 100644 --- a/core/field_colour.ts +++ b/core/field_colour.ts @@ -80,7 +80,7 @@ export class FieldColour extends Field { static COLUMNS = 7; /** The field's colour picker element. */ - private picker_: Element|null = null; + private picker_: HTMLElement|null = null; /** Index of the currently highlighted element. */ private highlightedIndex_: number|null = null; @@ -134,9 +134,6 @@ export class FieldColour extends Field { * setting. By default use the global constants for columns. */ private columns_ = 0; - override size_: AnyDuringMigration; - override clickTarget_: AnyDuringMigration; - override value_: AnyDuringMigration; /** * @param opt_value The initial value of the field. Should be in '#rrggbb' @@ -157,9 +154,7 @@ export class FieldColour extends Field { opt_config?: FieldColourConfig) { super(Field.SKIP_SETUP); - if (opt_value === Field.SKIP_SETUP) { - return; - } + if (Field.isSentinel(opt_value)) return; if (opt_config) { this.configure_(opt_config); } @@ -230,15 +225,14 @@ export class FieldColour extends Field { * @param newValue The value to be saved. The default validator guarantees * that this is a colour in '#rrggbb' format. */ - protected override doValueUpdate_(newValue: AnyDuringMigration) { + protected override doValueUpdate_(newValue: string) { this.value_ = newValue; if (this.borderRect_) { - this.borderRect_.style.fill = newValue as string; + this.borderRect_.style.fill = newValue; } else if ( this.sourceBlock_ && this.sourceBlock_.rendered && this.sourceBlock_ instanceof BlockSvg) { - this.sourceBlock_.pathObject.svgPath.setAttribute( - 'fill', newValue as string); + this.sourceBlock_.pathObject.svgPath.setAttribute('fill', newValue); this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff'); } } @@ -289,16 +283,12 @@ export class FieldColour extends Field { /** Create and show the colour field's editor. */ protected override showEditor_() { this.dropdownCreate_(); - // AnyDuringMigration because: Argument of type 'Element | null' is not - // assignable to parameter of type 'Node'. - dropDownDiv.getContentDiv().appendChild(this.picker_ as AnyDuringMigration); + dropDownDiv.getContentDiv().appendChild(this.picker_!); dropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this)); // Focus so we can start receiving keyboard events. - // AnyDuringMigration because: Property 'focus' does not exist on type - // 'Element'. - (this.picker_ as AnyDuringMigration)!.focus({preventScroll: true}); + this.picker_!.focus({preventScroll: true}); } /** @@ -519,9 +509,7 @@ export class FieldColour extends Field { cell.setAttribute('data-colour', colours[i]); cell.title = titles[i] || colours[i]; cell.id = idGenerator.getNextUniqueId(); - // AnyDuringMigration because: Argument of type 'number' is not - // assignable to parameter of type 'string'. - cell.setAttribute('data-index', i as AnyDuringMigration); + cell.setAttribute('data-index', String(i)); aria.setRole(cell, aria.Role.GRIDCELL); aria.setState(cell, aria.State.LABEL, colours[i]); aria.setState(cell, aria.State.SELECTED, colours[i] === selectedColour); @@ -584,7 +572,7 @@ export class FieldColour extends Field { 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); + return new this(options.colour, undefined, options); } } diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index eb14d766f..ced3a4bfe 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -92,7 +92,7 @@ export class FieldDropdown extends Field { */ override suffixField: string|null = null; // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. - private selectedOption_!: Array; + private selectedOption_!: MenuOption; override clickTarget_: SVGElement|null = null; /** @@ -398,8 +398,7 @@ export class FieldDropdown extends Field { * @param opt_newValue The input value. * @returns A valid language-neutral option, or null if invalid. */ - protected override doClassValidation_(opt_newValue?: MenuOption[1]): string - |null { + protected override doClassValidation_(opt_newValue?: string): string|null { const options = this.getOptions(true); const isValueValid = options.some((option) => option[1] === opt_newValue); @@ -421,7 +420,7 @@ export class FieldDropdown extends Field { * @param newValue The value to be saved. The default validator guarantees * that this is one of the valid dropdown options. */ - protected override doValueUpdate_(newValue: MenuOption[1]) { + protected override doValueUpdate_(newValue: string) { super.doValueUpdate_(newValue); const options = this.getOptions(true); for (let i = 0, option; option = options[i]; i++) { @@ -465,7 +464,7 @@ export class FieldDropdown extends Field { // Show correct element. const option = this.selectedOption_ && this.selectedOption_[0]; if (option && typeof option === 'object') { - this.renderSelectedImage_((option)); + this.renderSelectedImage_(option); } else { this.renderSelectedText_(); } diff --git a/core/field_image.ts b/core/field_image.ts index 728be7e94..ab0a6bb37 100644 --- a/core/field_image.ts +++ b/core/field_image.ts @@ -60,7 +60,6 @@ export class FieldImage extends Field { /** Alt text of this image. */ private altText_ = ''; - override value_: AnyDuringMigration; /** * @param src The URL of the image. @@ -179,7 +178,7 @@ export class FieldImage extends Field { * @param newValue The value to be saved. The default validator guarantees * that this is a string. */ - protected override doValueUpdate_(newValue: AnyDuringMigration) { + protected override doValueUpdate_(newValue: string) { this.value_ = newValue; if (this.imageElement_) { this.imageElement_.setAttributeNS( diff --git a/core/field_input.ts b/core/field_input.ts index b7e36d05b..88946b47a 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -32,14 +32,16 @@ import * as WidgetDiv from './widgetdiv.js'; import type {WorkspaceSvg} from './workspace_svg.js'; export type InputTypes = string|number; -export type FieldInputValidator = FieldValidator; +export type FieldInputValidator = + FieldValidator; /** - * Class for an editable text field. + * Abstract class for an editable input field. * * @alias Blockly.FieldInput + * @typeParam T - The value stored on the field. */ -export abstract class FieldInput extends Field { +export abstract class FieldInput extends Field { /** * Pixel size of input border radius. * Should match blocklyText's border-radius in CSS. @@ -83,9 +85,6 @@ export abstract class FieldInput extends Field { /** Mouse cursor style when over the hotspot that initiates the editor. */ override CURSOR = 'text'; - override clickTarget_: AnyDuringMigration; - override value_: AnyDuringMigration; - override isDirty_: AnyDuringMigration; /** * @param opt_value The initial value of the field. Should cast to a string. @@ -106,9 +105,7 @@ export abstract class FieldInput extends Field { opt_config?: FieldInputConfig) { super(Field.SKIP_SETUP); - if (opt_value === Field.SKIP_SETUP) { - return; - } + if (Field.isSentinel(opt_value)) return; if (opt_config) { this.configure_(opt_config); } @@ -161,20 +158,6 @@ export abstract class FieldInput extends Field { this.createTextElement_(); } - /** - * Ensure that the input value casts to a valid string. - * - * @param opt_newValue The input value. - * @returns A valid string, or null if invalid. - */ - protected override doClassValidation_(opt_newValue?: AnyDuringMigration): - AnyDuringMigration { - if (opt_newValue === null || opt_newValue === undefined) { - return null; - } - return String(opt_newValue); - } - /** * Called by setValue if the text input is not valid. If the field is * currently being edited it reverts value of the field to the previous @@ -207,7 +190,7 @@ export abstract class FieldInput extends Field { * @param newValue The value to be saved. The default validator guarantees * that this is a string. */ - protected override doValueUpdate_(newValue: AnyDuringMigration) { + protected override doValueUpdate_(newValue: string|T) { this.isDirty_ = true; this.isTextValid_ = true; this.value_ = newValue; @@ -380,7 +363,7 @@ export abstract class FieldInput extends Field { div!.appendChild(htmlInput); htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_); - htmlInput.setAttribute('data-untyped-default-value', this.value_); + htmlInput.setAttribute('data-untyped-default-value', String(this.value_)); this.resizeEditor_(); @@ -457,29 +440,19 @@ export abstract class FieldInput extends Field { * * @param e Keyboard event. */ - protected onHtmlInputKeyDown_(e: Event) { - // AnyDuringMigration because: Property 'keyCode' does not exist on type - // 'Event'. - if ((e as AnyDuringMigration).keyCode === KeyCodes.ENTER) { + protected onHtmlInputKeyDown_(e: KeyboardEvent) { + if (e.keyCode === KeyCodes.ENTER) { WidgetDiv.hide(); dropDownDiv.hideWithoutAnimation(); - // AnyDuringMigration because: Property 'keyCode' does not exist on type - // 'Event'. - } else if ((e as AnyDuringMigration).keyCode === KeyCodes.ESC) { + } else if (e.keyCode === KeyCodes.ESC) { this.setValue( this.htmlInput_!.getAttribute('data-untyped-default-value')); WidgetDiv.hide(); dropDownDiv.hideWithoutAnimation(); - // AnyDuringMigration because: Property 'keyCode' does not exist on type - // 'Event'. - } else if ((e as AnyDuringMigration).keyCode === KeyCodes.TAB) { + } else if (e.keyCode === KeyCodes.TAB) { WidgetDiv.hide(); dropDownDiv.hideWithoutAnimation(); - // AnyDuringMigration because: Property 'shiftKey' does not exist on type - // 'Event'. AnyDuringMigration because: Argument of type 'this' is not - // assignable to parameter of type 'Field'. - (this.sourceBlock_ as BlockSvg) - .tab(this as AnyDuringMigration, !(e as AnyDuringMigration).shiftKey); + (this.sourceBlock_ as BlockSvg).tab(this, !e.shiftKey); e.preventDefault(); } } diff --git a/core/field_label.ts b/core/field_label.ts index 2a4730ff2..22a55a3b7 100644 --- a/core/field_label.ts +++ b/core/field_label.ts @@ -51,9 +51,7 @@ export class FieldLabel extends Field { opt_config?: FieldLabelConfig) { super(Field.SKIP_SETUP); - if (opt_value === Field.SKIP_SETUP) { - return; - } + if (Field.isSentinel(opt_value)) return; if (opt_config) { this.configure_(opt_config); } else { diff --git a/core/field_multilineinput.ts b/core/field_multilineinput.ts index abd53193a..e6473811b 100644 --- a/core/field_multilineinput.ts +++ b/core/field_multilineinput.ts @@ -70,9 +70,7 @@ export class FieldMultilineInput extends FieldTextInput { opt_config?: FieldMultilineInputConfig) { super(Field.SKIP_SETUP); - if (opt_value === Field.SKIP_SETUP) { - return; - } + if (Field.isSentinel(opt_value)) return; if (opt_config) { this.configure_(opt_config); } @@ -210,9 +208,11 @@ export class FieldMultilineInput extends FieldTextInput { * @param newValue The value to be saved. The default validator guarantees * that this is a string. */ - protected override doValueUpdate_(newValue: AnyDuringMigration) { + protected override doValueUpdate_(newValue: string) { super.doValueUpdate_(newValue); - this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_; + if (this.value_ !== null) { + this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_; + } } /** Updates the text of the textElement. */ @@ -300,7 +300,7 @@ export class FieldMultilineInput extends FieldTextInput { // 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 = this.value_.split('\n'); + const actualEditorLines = String(this.value_).split('\n'); const dummyTextElement = dom.createSvgElement( Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'}); @@ -385,7 +385,7 @@ export class FieldMultilineInput extends FieldTextInput { div!.appendChild(htmlInput); htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_); - htmlInput.setAttribute('data-untyped-default-value', 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. @@ -428,10 +428,8 @@ export class FieldMultilineInput extends FieldTextInput { * * @param e Keyboard event. */ - protected override onHtmlInputKeyDown_(e: Event) { - // AnyDuringMigration because: Property 'keyCode' does not exist on type - // 'Event'. - if ((e as AnyDuringMigration).keyCode !== KeyCodes.ENTER) { + protected override onHtmlInputKeyDown_(e: KeyboardEvent) { + if (e.keyCode !== KeyCodes.ENTER) { super.onHtmlInputKeyDown_(e); } } diff --git a/core/field_number.ts b/core/field_number.ts index a891f1c58..20b799555 100644 --- a/core/field_number.ts +++ b/core/field_number.ts @@ -77,9 +77,7 @@ export class FieldNumber extends FieldInput { // Pass SENTINEL so that we can define properties before value validation. super(Field.SKIP_SETUP); - if (opt_value === Field.SKIP_SETUP) { - return; - } + if (Field.isSentinel(opt_value)) return; if (opt_config) { this.configure_(opt_config); } else { @@ -260,6 +258,7 @@ export class FieldNumber extends FieldInput { if (opt_newValue === null) { return null; } + // Clean up text. let newValue = String(opt_newValue); // TODO: Handle cases like 'ten', '1.203,14', etc. diff --git a/core/field_textinput.ts b/core/field_textinput.ts index 2a2f1c6db..5ef633dc4 100644 --- a/core/field_textinput.ts +++ b/core/field_textinput.ts @@ -22,6 +22,11 @@ import type {Sentinel} from './utils/sentinel.js'; export type FieldTextInputValidator = FieldInputValidator; +/** + * Class for an editable text field. + * + * @alias Blockly.FieldTextInput + */ export class FieldTextInput extends FieldInput { /** * @param opt_value The initial value of the field. Should cast to a string. @@ -43,6 +48,20 @@ export class FieldTextInput extends FieldInput { super(opt_value, opt_validator, opt_config); } + /** + * Ensure that the input value casts to a valid string. + * + * @param opt_newValue The input value. + * @returns A valid string, or null if invalid. + */ + protected override doClassValidation_(opt_newValue?: AnyDuringMigration): + string|null { + if (opt_newValue === undefined) { + return null; + } + return String(opt_newValue); + } + /** * Construct a FieldTextInput from a JSON arg object, * dereferencing any string table references. diff --git a/core/field_variable.ts b/core/field_variable.ts index e3b98f4fe..8d2fd276c 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -306,7 +306,7 @@ export class FieldVariable extends FieldDropdown { * * @returns Validation function, or null. */ - override getValidator(): Function|null { + override getValidator(): FieldVariableValidator|null { // Validators shouldn't operate on the initial setValue call. // Normally this is achieved by calling setValidator after setValue, but // this is not a possibility with variable fields. @@ -357,7 +357,7 @@ export class FieldVariable extends FieldDropdown { * * @param newId The value to be saved. */ - protected override doValueUpdate_(newId: AnyDuringMigration) { + protected override doValueUpdate_(newId: string) { const block = this.getSourceBlock(); if (!block) { throw new UnattachedFieldError(); diff --git a/core/gesture.ts b/core/gesture.ts index 935aa37d8..065434097 100644 --- a/core/gesture.ts +++ b/core/gesture.ts @@ -969,14 +969,14 @@ export class Gesture { * @param field The field the gesture started on. * @internal */ - setStartField(field: Field) { + setStartField(field: Field) { if (this.hasStarted_) { throw Error( 'Tried to call gesture.setStartField, ' + 'but the gesture had already been started.'); } if (!this.startField_) { - this.startField_ = field; + this.startField_ = field as Field; } } diff --git a/core/utils/sentinel.ts b/core/utils/sentinel.ts index f6b8da26d..723c29986 100644 --- a/core/utils/sentinel.ts +++ b/core/utils/sentinel.ts @@ -18,4 +18,10 @@ goog.declareModuleId('Blockly.utils.Sentinel'); * * @alias Blockly.utils.Sentinel */ -export class Sentinel {} +export class Sentinel { + /** + * Provide a unique key so that type guarding properly excludes values like + * string. + */ + UNIQUE_KEY?: never; +}