fix: replace 'AnyDuringMigration' for core/field.ts value functions (#6639)

* chore: replace `AnyDuringMigration` for field value functions

* chore: removed unnecessary `KeyboardEvent` comments and `AnyDuringMigration` casts

* chore: cleaned up `doValueUpdate_` and `doClassValidation_` in `Field` subclasses

* fix: updated `FieldValidator` to allow returning `undefined` and restructured `setValue`

* fix: implemented initial `core/field_checkbox.ts` feedback

* fix: updated `Field` to accept `U` and `undefined` and reverted subclass constructor handling of the input value

* fix: reverted `getVars` to returning `string[]` and added related comment

* chore: removed unnecessary comment

* fix: updated `processValidation_` to no longer allow returning `undefined`

* fix: removed `Un` type alias for `undefined`

* chore: removed unnecessary string cast in `core/field_colour.ts`

* fix: updated `doClassValidation_` not to expect `null` since it will never come up in `setValue`

* fix: updated `doClassValidation_` to only allow `undefined` when the new value exists

* Updated `FieldValidator` type to expect `newValue` to exist

* cleanup: updated `picker` from type `Element` to type `HTMLElement`

* fix: updated `doValueInvalid_` type info in `core/field_input.ts` to handle `string` and `undefined`

* fix: reverted `getValue` in `core/field_checkbox.ts` to previous logic

* fix: updated the `Field` constructor to allow `value` to be optional

* chore: consolidated `Validation` with `FieldValidator` and `doClassValidation_`

* fix: reverted generic param `U` while handling diverging user input is being discussed

* fix: updated `doClassValidation_` return comment to work for TSDoc

* fix: misc keyboard event function tweaks
This commit is contained in:
Blake Thomas Williams
2023-01-10 16:13:49 -06:00
committed by GitHub
parent 25d9acb418
commit 0fb64a6772
15 changed files with 169 additions and 162 deletions

View File

@@ -1066,11 +1066,12 @@ export class Block implements IASTNodeLocation, IDeletable {
* @returns List of variable ids. * @returns List of variable ids.
*/ */
getVars(): string[] { getVars(): string[] {
const vars = []; const vars: string[] = [];
for (let i = 0, input; input = this.inputList[i]; i++) { for (let i = 0, input; input = this.inputList[i]; i++) {
for (let j = 0, field; field = input.fieldRow[j]; j++) { for (let j = 0, field; field = input.fieldRow[j]; j++) {
if (field.referencesVariables()) { if (field.referencesVariables()) {
vars.push(field.getValue()); // NOTE: This only applies to `FieldVariable`, a `Field<string>`
vars.push(field.getValue() as string);
} }
} }
} }

View File

@@ -45,12 +45,30 @@ import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
import * as Xml from './xml.js'; import * as Xml from './xml.js';
export type FieldValidator<T = any> = (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<T = any> = (newValue: T) => T|null|undefined;
/** /**
* Abstract class for an editable field. * Abstract class for an editable field.
* *
* @alias Blockly.Field * @alias Blockly.Field
* @typeParam T - The value stored on the field.
*/ */
export abstract class Field<T = any> implements IASTNodeLocationSvg, export abstract class Field<T = any> implements IASTNodeLocationSvg,
IASTNodeLocationWithBlock, IASTNodeLocationWithBlock,
@@ -76,6 +94,9 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* instead. * instead.
*/ */
static readonly SKIP_SETUP = new Sentinel(); static readonly SKIP_SETUP = new Sentinel();
static isSentinel<T>(value: T|Sentinel): value is Sentinel {
return value === Field.SKIP_SETUP;
}
/** /**
* Name of field. Unique within each block. * Name of field. Unique within each block.
@@ -207,9 +228,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
/** The size of the area rendered by the field. */ /** The size of the area rendered by the field. */
this.size_ = new Size(0, 0); this.size_ = new Size(0, 0);
if (value === Field.SKIP_SETUP) { if (Field.isSentinel(value)) return;
return;
}
if (opt_config) { if (opt_config) {
this.configure_(opt_config); this.configure_(opt_config);
} }
@@ -372,7 +391,8 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* @internal * @internal
*/ */
fromXml(fieldElement: Element) { 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<T = any> implements IASTNodeLocationSvg,
* @internal * @internal
*/ */
toXml(fieldElement: Element): Element { toXml(fieldElement: Element): Element {
fieldElement.textContent = this.getValue(); // Any because gremlins live here. No touchie!
fieldElement.textContent = this.getValue() as any;
return fieldElement; return fieldElement;
} }
@@ -619,7 +640,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* *
* @returns Validation function, or null. * @returns Validation function, or null.
*/ */
getValidator(): Function|null { getValidator(): FieldValidator<T>|null {
return this.validator_; return this.validator_;
} }
@@ -965,41 +986,37 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
return; return;
} }
let validatedValue = this.doClassValidation_(newValue); const classValidation = this.doClassValidation_(newValue);
// Class validators might accidentally forget to return, we'll ignore that. const classValue = this.processValidation_(newValue, classValidation);
newValue = this.processValidation_(newValue, validatedValue); if (classValue instanceof Error) {
if (newValue instanceof Error) {
doLogging && console.log('invalid class validation, return'); doLogging && console.log('invalid class validation, return');
return; return;
} }
const localValidator = this.getValidator(); const localValidation = this.getValidator()?.call(this, classValue);
if (localValidator) { const localValue = this.processValidation_(classValue, localValidation);
validatedValue = localValidator.call(this, newValue); if (localValue instanceof Error) {
// Local validators might accidentally forget to return, we'll ignore doLogging && console.log('invalid local validation, return');
// that. return;
newValue = this.processValidation_(newValue, validatedValue);
if (newValue instanceof Error) {
doLogging && console.log('invalid local validation, return');
return;
}
} }
const source = this.sourceBlock_; const source = this.sourceBlock_;
if (source && source.disposed) { if (source && source.disposed) {
doLogging && console.log('source disposed, return'); doLogging && console.log('source disposed, return');
return; return;
} }
const oldValue = this.getValue(); const oldValue = this.getValue();
if (oldValue === newValue) { if (oldValue === localValue) {
doLogging && console.log('same, doValueUpdate_, return'); doLogging && console.log('same, doValueUpdate_, return');
this.doValueUpdate_(newValue); this.doValueUpdate_(localValue);
return; return;
} }
this.doValueUpdate_(newValue); this.doValueUpdate_(localValue);
if (source && eventUtils.isEnabled()) { if (source && eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))( 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_) { if (this.isDirty_) {
this.forceRerender(); this.forceRerender();
@@ -1015,8 +1032,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* @returns New value, or an Error object. * @returns New value, or an Error object.
*/ */
private processValidation_( private processValidation_(
newValue: AnyDuringMigration, newValue: AnyDuringMigration, validatedValue: T|null|undefined): T|Error {
validatedValue: AnyDuringMigration): AnyDuringMigration {
if (validatedValue === null) { if (validatedValue === null) {
this.doValueInvalid_(newValue); this.doValueInvalid_(newValue);
if (this.isDirty_) { if (this.isDirty_) {
@@ -1024,10 +1040,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
} }
return Error(); return Error();
} }
if (validatedValue !== undefined) { return validatedValue === undefined ? newValue as T : validatedValue;
newValue = validatedValue;
}
return newValue;
} }
/** /**
@@ -1035,23 +1048,39 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* *
* @returns Current value. * @returns Current value.
*/ */
getValue(): AnyDuringMigration { getValue(): T|null {
return this.value_; return this.value_;
} }
/** /**
* Used to validate a value. Returns input by default. Can be overridden by * Validate the changes to a field's value before they are set. See
* subclasses, see FieldDropdown. * **FieldDropdown** for an example of subclass implementation.
* *
* @param opt_newValue The value to be validated. * **NOTE:** Validation returns one option between `T`, `null`, and
* @returns The validated value, same as input by default. * `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): protected doClassValidation_(newValue: T): T|null|undefined;
AnyDuringMigration { protected doClassValidation_(newValue?: AnyDuringMigration): T|null;
if (opt_newValue === null || opt_newValue === undefined) { protected doClassValidation_(newValue?: T|AnyDuringMigration): T|null
|undefined {
if (newValue === null || newValue === undefined) {
return null; return null;
} }
return opt_newValue;
return newValue as T;
} }
/** /**
@@ -1060,7 +1089,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
* *
* @param newValue The value to be saved. * @param newValue The value to be saved.
*/ */
protected doValueUpdate_(newValue: AnyDuringMigration) { protected doValueUpdate_(newValue: T) {
this.value_ = newValue; this.value_ = newValue;
this.isDirty_ = true; this.isDirty_ = true;
} }
@@ -1086,7 +1115,7 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
} }
const gesture = (this.sourceBlock_.workspace as WorkspaceSvg).getGesture(e); const gesture = (this.sourceBlock_.workspace as WorkspaceSvg).getGesture(e);
if (gesture) { if (gesture) {
gesture.setStartField(this as Field); gesture.setStartField(this);
} }
} }

View File

@@ -137,9 +137,7 @@ export class FieldAngle extends FieldInput<number> {
opt_config?: FieldAngleConfig) { opt_config?: FieldAngleConfig) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) { if (Field.isSentinel(opt_value)) return;
return;
}
if (opt_config) { if (opt_config) {
this.configure_(opt_config); this.configure_(opt_config);
} }
@@ -404,25 +402,24 @@ export class FieldAngle extends FieldInput<number> {
* *
* @param e Keyboard event. * @param e Keyboard event.
*/ */
protected override onHtmlInputKeyDown_(e: Event) { protected override onHtmlInputKeyDown_(e: KeyboardEvent) {
super.onHtmlInputKeyDown_(e); super.onHtmlInputKeyDown_(e);
const block = this.getSourceBlock(); const block = this.getSourceBlock();
if (!block) { if (!block) {
throw new UnattachedFieldError(); throw new UnattachedFieldError();
} }
const keyboardEvent = e as KeyboardEvent;
let multiplier; let multiplier;
if (keyboardEvent.keyCode === KeyCodes.LEFT) { if (e.keyCode === KeyCodes.LEFT) {
// decrement (increment in RTL) // decrement (increment in RTL)
multiplier = block.RTL ? 1 : -1; multiplier = block.RTL ? 1 : -1;
} else if (keyboardEvent.keyCode === KeyCodes.RIGHT) { } else if (e.keyCode === KeyCodes.RIGHT) {
// increment (decrement in RTL) // increment (decrement in RTL)
multiplier = block.RTL ? -1 : 1; multiplier = block.RTL ? -1 : 1;
} else if (keyboardEvent.keyCode === KeyCodes.DOWN) { } else if (e.keyCode === KeyCodes.DOWN) {
// decrement // decrement
multiplier = -1; multiplier = -1;
} else if (keyboardEvent.keyCode === KeyCodes.UP) { } else if (e.keyCode === KeyCodes.UP) {
// increment // increment
multiplier = 1; multiplier = 1;
} }

View File

@@ -20,14 +20,16 @@ import {Field, FieldConfig, FieldValidator} from './field.js';
import * as fieldRegistry from './field_registry.js'; import * as fieldRegistry from './field_registry.js';
import type {Sentinel} from './utils/sentinel.js'; import type {Sentinel} from './utils/sentinel.js';
export type FieldCheckboxValidator = FieldValidator<boolean>; type BoolString = 'TRUE'|'FALSE';
type CheckboxBool = BoolString|boolean;
export type FieldCheckboxValidator = FieldValidator<CheckboxBool>;
/** /**
* Class for a checkbox field. * Class for a checkbox field.
* *
* @alias Blockly.FieldCheckbox * @alias Blockly.FieldCheckbox
*/ */
export class FieldCheckbox extends Field<boolean> { export class FieldCheckbox extends Field<CheckboxBool> {
/** Default character for the checkmark. */ /** Default character for the checkmark. */
static readonly CHECK_CHAR = '✓'; static readonly CHECK_CHAR = '✓';
private checkChar_: string; private checkChar_: string;
@@ -42,7 +44,12 @@ export class FieldCheckbox extends Field<boolean> {
* Mouse cursor style when over the hotspot that initiates editability. * Mouse cursor style when over the hotspot that initiates editability.
*/ */
override CURSOR = 'default'; 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', * @param opt_value The initial value of the field. Should either be 'TRUE',
@@ -59,8 +66,7 @@ export class FieldCheckbox extends Field<boolean> {
* for a list of properties this parameter supports. * for a list of properties this parameter supports.
*/ */
constructor( constructor(
opt_value?: string|boolean|Sentinel, opt_value?: CheckboxBool|Sentinel, opt_validator?: FieldCheckboxValidator,
opt_validator?: FieldCheckboxValidator,
opt_config?: FieldCheckboxConfig) { opt_config?: FieldCheckboxConfig) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
@@ -70,9 +76,7 @@ export class FieldCheckbox extends Field<boolean> {
*/ */
this.checkChar_ = FieldCheckbox.CHECK_CHAR; this.checkChar_ = FieldCheckbox.CHECK_CHAR;
if (opt_value === Field.SKIP_SETUP) { if (Field.isSentinel(opt_value)) return;
return;
}
if (opt_config) { if (opt_config) {
this.configure_(opt_config); this.configure_(opt_config);
} }
@@ -153,7 +157,7 @@ export class FieldCheckbox extends Field<boolean> {
* @returns A valid value ('TRUE' or 'FALSE), or null if invalid. * @returns A valid value ('TRUE' or 'FALSE), or null if invalid.
*/ */
protected override doClassValidation_(opt_newValue?: AnyDuringMigration): protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
string|null { BoolString|null {
if (opt_newValue === true || opt_newValue === 'TRUE') { if (opt_newValue === true || opt_newValue === 'TRUE') {
return 'TRUE'; return 'TRUE';
} }
@@ -169,7 +173,7 @@ export class FieldCheckbox extends Field<boolean> {
* @param newValue The value to be saved. The default validator guarantees * @param newValue The value to be saved. The default validator guarantees
* that this is a either 'TRUE' or 'FALSE'. * that this is a either 'TRUE' or 'FALSE'.
*/ */
protected override doValueUpdate_(newValue: AnyDuringMigration) { protected override doValueUpdate_(newValue: BoolString) {
this.value_ = this.convertValueToBool_(newValue); this.value_ = this.convertValueToBool_(newValue);
// Update visual. // Update visual.
if (this.textElement_) { if (this.textElement_) {
@@ -182,7 +186,7 @@ export class FieldCheckbox extends Field<boolean> {
* *
* @returns The value of this field. * @returns The value of this field.
*/ */
override getValue(): string { override getValue(): BoolString {
return this.value_ ? 'TRUE' : 'FALSE'; return this.value_ ? 'TRUE' : 'FALSE';
} }
@@ -191,8 +195,8 @@ export class FieldCheckbox extends Field<boolean> {
* *
* @returns The boolean value of this field. * @returns The boolean value of this field.
*/ */
getValueBoolean(): boolean { getValueBoolean(): boolean|null {
return this.value_ as boolean; return this.value_;
} }
/** /**
@@ -213,12 +217,9 @@ export class FieldCheckbox extends Field<boolean> {
* @param value The value to convert. * @param value The value to convert.
* @returns The converted value. * @returns The converted value.
*/ */
private convertValueToBool_(value: AnyDuringMigration): boolean { private convertValueToBool_(value: CheckboxBool|null): boolean {
if (typeof value === 'string') { if (typeof value === 'string') return value === 'TRUE';
return value === 'TRUE'; return !!value;
} else {
return !!value;
}
} }
/** /**

View File

@@ -80,7 +80,7 @@ export class FieldColour extends Field<string> {
static COLUMNS = 7; static COLUMNS = 7;
/** The field's colour picker element. */ /** The field's colour picker element. */
private picker_: Element|null = null; private picker_: HTMLElement|null = null;
/** Index of the currently highlighted element. */ /** Index of the currently highlighted element. */
private highlightedIndex_: number|null = null; private highlightedIndex_: number|null = null;
@@ -134,9 +134,6 @@ export class FieldColour extends Field<string> {
* setting. By default use the global constants for columns. * setting. By default use the global constants for columns.
*/ */
private columns_ = 0; 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' * @param opt_value The initial value of the field. Should be in '#rrggbb'
@@ -157,9 +154,7 @@ export class FieldColour extends Field<string> {
opt_config?: FieldColourConfig) { opt_config?: FieldColourConfig) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) { if (Field.isSentinel(opt_value)) return;
return;
}
if (opt_config) { if (opt_config) {
this.configure_(opt_config); this.configure_(opt_config);
} }
@@ -230,15 +225,14 @@ export class FieldColour extends Field<string> {
* @param newValue The value to be saved. The default validator guarantees * @param newValue The value to be saved. The default validator guarantees
* that this is a colour in '#rrggbb' format. * that this is a colour in '#rrggbb' format.
*/ */
protected override doValueUpdate_(newValue: AnyDuringMigration) { protected override doValueUpdate_(newValue: string) {
this.value_ = newValue; this.value_ = newValue;
if (this.borderRect_) { if (this.borderRect_) {
this.borderRect_.style.fill = newValue as string; this.borderRect_.style.fill = newValue;
} else if ( } else if (
this.sourceBlock_ && this.sourceBlock_.rendered && this.sourceBlock_ && this.sourceBlock_.rendered &&
this.sourceBlock_ instanceof BlockSvg) { this.sourceBlock_ instanceof BlockSvg) {
this.sourceBlock_.pathObject.svgPath.setAttribute( this.sourceBlock_.pathObject.svgPath.setAttribute('fill', newValue);
'fill', newValue as string);
this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff'); this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff');
} }
} }
@@ -289,16 +283,12 @@ export class FieldColour extends Field<string> {
/** Create and show the colour field's editor. */ /** Create and show the colour field's editor. */
protected override showEditor_() { protected override showEditor_() {
this.dropdownCreate_(); this.dropdownCreate_();
// AnyDuringMigration because: Argument of type 'Element | null' is not dropDownDiv.getContentDiv().appendChild(this.picker_!);
// assignable to parameter of type 'Node'.
dropDownDiv.getContentDiv().appendChild(this.picker_ as AnyDuringMigration);
dropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this)); dropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this));
// Focus so we can start receiving keyboard events. // Focus so we can start receiving keyboard events.
// AnyDuringMigration because: Property 'focus' does not exist on type this.picker_!.focus({preventScroll: true});
// 'Element'.
(this.picker_ as AnyDuringMigration)!.focus({preventScroll: true});
} }
/** /**
@@ -519,9 +509,7 @@ export class FieldColour extends Field<string> {
cell.setAttribute('data-colour', colours[i]); cell.setAttribute('data-colour', colours[i]);
cell.title = titles[i] || colours[i]; cell.title = titles[i] || colours[i];
cell.id = idGenerator.getNextUniqueId(); cell.id = idGenerator.getNextUniqueId();
// AnyDuringMigration because: Argument of type 'number' is not cell.setAttribute('data-index', String(i));
// assignable to parameter of type 'string'.
cell.setAttribute('data-index', i as AnyDuringMigration);
aria.setRole(cell, aria.Role.GRIDCELL); aria.setRole(cell, aria.Role.GRIDCELL);
aria.setState(cell, aria.State.LABEL, colours[i]); aria.setState(cell, aria.State.LABEL, colours[i]);
aria.setState(cell, aria.State.SELECTED, colours[i] === selectedColour); aria.setState(cell, aria.State.SELECTED, colours[i] === selectedColour);
@@ -584,7 +572,7 @@ export class FieldColour extends Field<string> {
static fromJson(options: FieldColourFromJsonConfig): FieldColour { static fromJson(options: FieldColourFromJsonConfig): FieldColour {
// `this` might be a subclass of FieldColour if that class doesn't override // `this` might be a subclass of FieldColour if that class doesn't override
// the static fromJson method. // the static fromJson method.
return new this(options['colour'], undefined, options); return new this(options.colour, undefined, options);
} }
} }

View File

@@ -92,7 +92,7 @@ export class FieldDropdown extends Field<string> {
*/ */
override suffixField: string|null = null; override suffixField: string|null = null;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix. // TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
private selectedOption_!: Array<string|ImageProperties>; private selectedOption_!: MenuOption;
override clickTarget_: SVGElement|null = null; override clickTarget_: SVGElement|null = null;
/** /**
@@ -398,8 +398,7 @@ export class FieldDropdown extends Field<string> {
* @param opt_newValue The input value. * @param opt_newValue The input value.
* @returns A valid language-neutral option, or null if invalid. * @returns A valid language-neutral option, or null if invalid.
*/ */
protected override doClassValidation_(opt_newValue?: MenuOption[1]): string protected override doClassValidation_(opt_newValue?: string): string|null {
|null {
const options = this.getOptions(true); const options = this.getOptions(true);
const isValueValid = options.some((option) => option[1] === opt_newValue); const isValueValid = options.some((option) => option[1] === opt_newValue);
@@ -421,7 +420,7 @@ export class FieldDropdown extends Field<string> {
* @param newValue The value to be saved. The default validator guarantees * @param newValue The value to be saved. The default validator guarantees
* that this is one of the valid dropdown options. * that this is one of the valid dropdown options.
*/ */
protected override doValueUpdate_(newValue: MenuOption[1]) { protected override doValueUpdate_(newValue: string) {
super.doValueUpdate_(newValue); super.doValueUpdate_(newValue);
const options = this.getOptions(true); const options = this.getOptions(true);
for (let i = 0, option; option = options[i]; i++) { for (let i = 0, option; option = options[i]; i++) {
@@ -465,7 +464,7 @@ export class FieldDropdown extends Field<string> {
// Show correct element. // Show correct element.
const option = this.selectedOption_ && this.selectedOption_[0]; const option = this.selectedOption_ && this.selectedOption_[0];
if (option && typeof option === 'object') { if (option && typeof option === 'object') {
this.renderSelectedImage_((option)); this.renderSelectedImage_(option);
} else { } else {
this.renderSelectedText_(); this.renderSelectedText_();
} }

View File

@@ -60,7 +60,6 @@ export class FieldImage extends Field<string> {
/** Alt text of this image. */ /** Alt text of this image. */
private altText_ = ''; private altText_ = '';
override value_: AnyDuringMigration;
/** /**
* @param src The URL of the image. * @param src The URL of the image.
@@ -179,7 +178,7 @@ export class FieldImage extends Field<string> {
* @param newValue The value to be saved. The default validator guarantees * @param newValue The value to be saved. The default validator guarantees
* that this is a string. * that this is a string.
*/ */
protected override doValueUpdate_(newValue: AnyDuringMigration) { protected override doValueUpdate_(newValue: string) {
this.value_ = newValue; this.value_ = newValue;
if (this.imageElement_) { if (this.imageElement_) {
this.imageElement_.setAttributeNS( this.imageElement_.setAttributeNS(

View File

@@ -32,14 +32,16 @@ import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
export type InputTypes = string|number; export type InputTypes = string|number;
export type FieldInputValidator<T extends InputTypes> = FieldValidator<T>; export type FieldInputValidator<T extends InputTypes> =
FieldValidator<string|T>;
/** /**
* Class for an editable text field. * Abstract class for an editable input field.
* *
* @alias Blockly.FieldInput * @alias Blockly.FieldInput
* @typeParam T - The value stored on the field.
*/ */
export abstract class FieldInput<T extends InputTypes> extends Field<T> { export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
/** /**
* Pixel size of input border radius. * Pixel size of input border radius.
* Should match blocklyText's border-radius in CSS. * Should match blocklyText's border-radius in CSS.
@@ -83,9 +85,6 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
/** Mouse cursor style when over the hotspot that initiates the editor. */ /** Mouse cursor style when over the hotspot that initiates the editor. */
override CURSOR = 'text'; 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. * @param opt_value The initial value of the field. Should cast to a string.
@@ -106,9 +105,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
opt_config?: FieldInputConfig) { opt_config?: FieldInputConfig) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) { if (Field.isSentinel(opt_value)) return;
return;
}
if (opt_config) { if (opt_config) {
this.configure_(opt_config); this.configure_(opt_config);
} }
@@ -161,20 +158,6 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
this.createTextElement_(); 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 * 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 * currently being edited it reverts value of the field to the previous
@@ -207,7 +190,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
* @param newValue The value to be saved. The default validator guarantees * @param newValue The value to be saved. The default validator guarantees
* that this is a string. * that this is a string.
*/ */
protected override doValueUpdate_(newValue: AnyDuringMigration) { protected override doValueUpdate_(newValue: string|T) {
this.isDirty_ = true; this.isDirty_ = true;
this.isTextValid_ = true; this.isTextValid_ = true;
this.value_ = newValue; this.value_ = newValue;
@@ -380,7 +363,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
div!.appendChild(htmlInput); div!.appendChild(htmlInput);
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_); 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_(); this.resizeEditor_();
@@ -457,29 +440,19 @@ export abstract class FieldInput<T extends InputTypes> extends Field<T> {
* *
* @param e Keyboard event. * @param e Keyboard event.
*/ */
protected onHtmlInputKeyDown_(e: Event) { protected onHtmlInputKeyDown_(e: KeyboardEvent) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type if (e.keyCode === KeyCodes.ENTER) {
// 'Event'.
if ((e as AnyDuringMigration).keyCode === KeyCodes.ENTER) {
WidgetDiv.hide(); WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation(); dropDownDiv.hideWithoutAnimation();
// AnyDuringMigration because: Property 'keyCode' does not exist on type } else if (e.keyCode === KeyCodes.ESC) {
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.ESC) {
this.setValue( this.setValue(
this.htmlInput_!.getAttribute('data-untyped-default-value')); this.htmlInput_!.getAttribute('data-untyped-default-value'));
WidgetDiv.hide(); WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation(); dropDownDiv.hideWithoutAnimation();
// AnyDuringMigration because: Property 'keyCode' does not exist on type } else if (e.keyCode === KeyCodes.TAB) {
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.TAB) {
WidgetDiv.hide(); WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation(); dropDownDiv.hideWithoutAnimation();
// AnyDuringMigration because: Property 'shiftKey' does not exist on type (this.sourceBlock_ as BlockSvg).tab(this, !e.shiftKey);
// 'Event'. AnyDuringMigration because: Argument of type 'this' is not
// assignable to parameter of type 'Field'.
(this.sourceBlock_ as BlockSvg)
.tab(this as AnyDuringMigration, !(e as AnyDuringMigration).shiftKey);
e.preventDefault(); e.preventDefault();
} }
} }

View File

@@ -51,9 +51,7 @@ export class FieldLabel extends Field<string> {
opt_config?: FieldLabelConfig) { opt_config?: FieldLabelConfig) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) { if (Field.isSentinel(opt_value)) return;
return;
}
if (opt_config) { if (opt_config) {
this.configure_(opt_config); this.configure_(opt_config);
} else { } else {

View File

@@ -70,9 +70,7 @@ export class FieldMultilineInput extends FieldTextInput {
opt_config?: FieldMultilineInputConfig) { opt_config?: FieldMultilineInputConfig) {
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) { if (Field.isSentinel(opt_value)) return;
return;
}
if (opt_config) { if (opt_config) {
this.configure_(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 * @param newValue The value to be saved. The default validator guarantees
* that this is a string. * that this is a string.
*/ */
protected override doValueUpdate_(newValue: AnyDuringMigration) { protected override doValueUpdate_(newValue: string) {
super.doValueUpdate_(newValue); 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. */ /** 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. // absolute longest line, even if it would be truncated after editing.
// Otherwise we would get wrong editor width when there are more // Otherwise we would get wrong editor width when there are more
// lines than this.maxLines_. // lines than this.maxLines_.
const actualEditorLines = this.value_.split('\n'); const actualEditorLines = String(this.value_).split('\n');
const dummyTextElement = dom.createSvgElement( const dummyTextElement = dom.createSvgElement(
Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'}); Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'});
@@ -385,7 +385,7 @@ export class FieldMultilineInput extends FieldTextInput {
div!.appendChild(htmlInput); div!.appendChild(htmlInput);
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_); 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', ''); htmlInput.setAttribute('data-old-value', '');
if (userAgent.GECKO) { if (userAgent.GECKO) {
// In FF, ensure the browser reflows before resizing to avoid issue #2777. // 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. * @param e Keyboard event.
*/ */
protected override onHtmlInputKeyDown_(e: Event) { protected override onHtmlInputKeyDown_(e: KeyboardEvent) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type if (e.keyCode !== KeyCodes.ENTER) {
// 'Event'.
if ((e as AnyDuringMigration).keyCode !== KeyCodes.ENTER) {
super.onHtmlInputKeyDown_(e); super.onHtmlInputKeyDown_(e);
} }
} }

View File

@@ -77,9 +77,7 @@ export class FieldNumber extends FieldInput<number> {
// Pass SENTINEL so that we can define properties before value validation. // Pass SENTINEL so that we can define properties before value validation.
super(Field.SKIP_SETUP); super(Field.SKIP_SETUP);
if (opt_value === Field.SKIP_SETUP) { if (Field.isSentinel(opt_value)) return;
return;
}
if (opt_config) { if (opt_config) {
this.configure_(opt_config); this.configure_(opt_config);
} else { } else {
@@ -260,6 +258,7 @@ export class FieldNumber extends FieldInput<number> {
if (opt_newValue === null) { if (opt_newValue === null) {
return null; return null;
} }
// Clean up text. // Clean up text.
let newValue = String(opt_newValue); let newValue = String(opt_newValue);
// TODO: Handle cases like 'ten', '1.203,14', etc. // TODO: Handle cases like 'ten', '1.203,14', etc.

View File

@@ -22,6 +22,11 @@ import type {Sentinel} from './utils/sentinel.js';
export type FieldTextInputValidator = FieldInputValidator<string>; export type FieldTextInputValidator = FieldInputValidator<string>;
/**
* Class for an editable text field.
*
* @alias Blockly.FieldTextInput
*/
export class FieldTextInput extends FieldInput<string> { export class FieldTextInput extends FieldInput<string> {
/** /**
* @param opt_value The initial value of the field. Should cast to a string. * @param opt_value The initial value of the field. Should cast to a string.
@@ -43,6 +48,20 @@ export class FieldTextInput extends FieldInput<string> {
super(opt_value, opt_validator, opt_config); 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, * Construct a FieldTextInput from a JSON arg object,
* dereferencing any string table references. * dereferencing any string table references.

View File

@@ -306,7 +306,7 @@ export class FieldVariable extends FieldDropdown {
* *
* @returns Validation function, or null. * @returns Validation function, or null.
*/ */
override getValidator(): Function|null { override getValidator(): FieldVariableValidator|null {
// Validators shouldn't operate on the initial setValue call. // Validators shouldn't operate on the initial setValue call.
// Normally this is achieved by calling setValidator after setValue, but // Normally this is achieved by calling setValidator after setValue, but
// this is not a possibility with variable fields. // this is not a possibility with variable fields.
@@ -357,7 +357,7 @@ export class FieldVariable extends FieldDropdown {
* *
* @param newId The value to be saved. * @param newId The value to be saved.
*/ */
protected override doValueUpdate_(newId: AnyDuringMigration) { protected override doValueUpdate_(newId: string) {
const block = this.getSourceBlock(); const block = this.getSourceBlock();
if (!block) { if (!block) {
throw new UnattachedFieldError(); throw new UnattachedFieldError();

View File

@@ -969,14 +969,14 @@ export class Gesture {
* @param field The field the gesture started on. * @param field The field the gesture started on.
* @internal * @internal
*/ */
setStartField(field: Field) { setStartField<T>(field: Field<T>) {
if (this.hasStarted_) { if (this.hasStarted_) {
throw Error( throw Error(
'Tried to call gesture.setStartField, ' + 'Tried to call gesture.setStartField, ' +
'but the gesture had already been started.'); 'but the gesture had already been started.');
} }
if (!this.startField_) { if (!this.startField_) {
this.startField_ = field; this.startField_ = field as Field;
} }
} }

View File

@@ -18,4 +18,10 @@ goog.declareModuleId('Blockly.utils.Sentinel');
* *
* @alias 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;
}