mirror of
https://github.com/google/blockly.git
synced 2026-01-10 18:37:09 +01:00
chore: add validator and field value types (#6603)
* chore: add validator and field value types * chore/clean up unused default and unnecessary field type setting * chore: removed IRegisterableField and updated various Field instances * fix: remove unused field image validator function * chore: added pass-through constructor to field_textinput
This commit is contained in:
committed by
GitHub
parent
0532b5d1c0
commit
79f620647f
@@ -1817,7 +1817,7 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
* @param element The element to try to turn into a field.
|
||||
* @returns The field defined by the JSON, or null if one couldn't be created.
|
||||
*/
|
||||
private fieldFromJson_(element: {alt?: string, type?: string, text?: string}):
|
||||
private fieldFromJson_(element: {alt?: string, type: string, text?: string}):
|
||||
Field|null {
|
||||
const field = fieldRegistry.fromJson(element);
|
||||
if (!field && element['alt']) {
|
||||
|
||||
@@ -53,19 +53,19 @@ import {DragTarget} from './drag_target.js';
|
||||
import * as dropDownDiv from './dropdowndiv.js';
|
||||
import * as Events from './events/events.js';
|
||||
import * as Extensions from './extensions.js';
|
||||
import {Field} from './field.js';
|
||||
import {FieldAngle} from './field_angle.js';
|
||||
import {FieldCheckbox} from './field_checkbox.js';
|
||||
import {FieldColour} from './field_colour.js';
|
||||
import {FieldDropdown, MenuGenerator, MenuGeneratorFunction, MenuOption} from './field_dropdown.js';
|
||||
import {Field, FieldValidator} from './field.js';
|
||||
import {FieldAngle, FieldAngleValidator} from './field_angle.js';
|
||||
import {FieldCheckbox, FieldCheckboxValidator} from './field_checkbox.js';
|
||||
import {FieldColour, FieldColourValidator} from './field_colour.js';
|
||||
import {FieldDropdown, FieldDropdownValidator, MenuGenerator, MenuGeneratorFunction, MenuOption} from './field_dropdown.js';
|
||||
import {FieldImage} from './field_image.js';
|
||||
import {FieldLabel} from './field_label.js';
|
||||
import {FieldLabelSerializable} from './field_label_serializable.js';
|
||||
import {FieldMultilineInput} from './field_multilineinput.js';
|
||||
import {FieldNumber} from './field_number.js';
|
||||
import {FieldMultilineInput, FieldMultilineInputValidator} from './field_multilineinput.js';
|
||||
import {FieldNumber, FieldNumberValidator} from './field_number.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {FieldTextInput} from './field_textinput.js';
|
||||
import {FieldVariable} from './field_variable.js';
|
||||
import {FieldTextInput, FieldTextInputValidator} from './field_textinput.js';
|
||||
import {FieldVariable, FieldVariableValidator} from './field_variable.js';
|
||||
import {Flyout} from './flyout_base.js';
|
||||
import {FlyoutButton} from './flyout_button.js';
|
||||
import {HorizontalFlyout} from './flyout_horizontal.js';
|
||||
@@ -101,7 +101,6 @@ import {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
||||
import {IMovable} from './interfaces/i_movable.js';
|
||||
import {IPositionable} from './interfaces/i_positionable.js';
|
||||
import {IRegistrable} from './interfaces/i_registrable.js';
|
||||
import {IRegistrableField} from './interfaces/i_registrable_field.js';
|
||||
import {ISelectable} from './interfaces/i_selectable.js';
|
||||
import {ISelectableToolboxItem} from './interfaces/i_selectable_toolbox_item.js';
|
||||
import {IStyleable} from './interfaces/i_styleable.js';
|
||||
@@ -648,18 +647,24 @@ export {Cursor};
|
||||
export {DeleteArea};
|
||||
export {DragTarget};
|
||||
export const DropDownDiv = dropDownDiv;
|
||||
export {Field};
|
||||
export {FieldAngle};
|
||||
export {FieldCheckbox};
|
||||
export {FieldColour};
|
||||
export {FieldDropdown, MenuGenerator, MenuGeneratorFunction, MenuOption};
|
||||
export {Field, FieldValidator};
|
||||
export {FieldAngle, FieldAngleValidator};
|
||||
export {FieldCheckbox, FieldCheckboxValidator};
|
||||
export {FieldColour, FieldColourValidator};
|
||||
export {
|
||||
FieldDropdown,
|
||||
FieldDropdownValidator,
|
||||
MenuGenerator,
|
||||
MenuGeneratorFunction,
|
||||
MenuOption,
|
||||
};
|
||||
export {FieldImage};
|
||||
export {FieldLabel};
|
||||
export {FieldLabelSerializable};
|
||||
export {FieldMultilineInput};
|
||||
export {FieldNumber};
|
||||
export {FieldTextInput};
|
||||
export {FieldVariable};
|
||||
export {FieldMultilineInput, FieldMultilineInputValidator};
|
||||
export {FieldNumber, FieldNumberValidator};
|
||||
export {FieldTextInput, FieldTextInputValidator};
|
||||
export {FieldVariable, FieldVariableValidator};
|
||||
export {Flyout};
|
||||
export {FlyoutButton};
|
||||
export {FlyoutMetricsManager};
|
||||
@@ -693,7 +698,6 @@ export {Input};
|
||||
export {InsertionMarkerManager};
|
||||
export {IPositionable};
|
||||
export {IRegistrable};
|
||||
export {IRegistrableField};
|
||||
export {ISelectable};
|
||||
export {ISelectableToolboxItem};
|
||||
export {IStyleable};
|
||||
|
||||
@@ -194,11 +194,12 @@ export function setColour(backgroundColour: string, borderColour: string) {
|
||||
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
|
||||
* @returns True if the menu rendered below block; false if above.
|
||||
*/
|
||||
export function showPositionedByBlock(
|
||||
field: Field, block: BlockSvg, opt_onHide?: Function,
|
||||
export function showPositionedByBlock<T>(
|
||||
field: Field<T>, block: BlockSvg, opt_onHide?: Function,
|
||||
opt_secondaryYOffset?: number): boolean {
|
||||
return showPositionedByRect(
|
||||
getScaledBboxOfBlock(block), field, opt_onHide, opt_secondaryYOffset);
|
||||
getScaledBboxOfBlock(block), field as Field, opt_onHide,
|
||||
opt_secondaryYOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,12 +213,13 @@ export function showPositionedByBlock(
|
||||
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
|
||||
* @returns True if the menu rendered below block; false if above.
|
||||
*/
|
||||
export function showPositionedByField(
|
||||
field: Field, opt_onHide?: Function,
|
||||
export function showPositionedByField<T>(
|
||||
field: Field<T>, opt_onHide?: Function,
|
||||
opt_secondaryYOffset?: number): boolean {
|
||||
positionToField = true;
|
||||
return showPositionedByRect(
|
||||
getScaledBboxOfField(field), field, opt_onHide, opt_secondaryYOffset);
|
||||
getScaledBboxOfField(field as Field), field as Field, opt_onHide,
|
||||
opt_secondaryYOffset);
|
||||
}
|
||||
/**
|
||||
* Get the scaled bounding box of a block.
|
||||
@@ -300,10 +302,10 @@ function showPositionedByRect(
|
||||
* @returns True if the menu rendered at the primary origin point.
|
||||
* @internal
|
||||
*/
|
||||
export function show(
|
||||
newOwner: Field, rtl: boolean, primaryX: number, primaryY: number,
|
||||
export function show<T>(
|
||||
newOwner: Field<T>, rtl: boolean, primaryX: number, primaryY: number,
|
||||
secondaryX: number, secondaryY: number, opt_onHide?: Function): boolean {
|
||||
owner = newOwner;
|
||||
owner = newOwner as Field;
|
||||
onHide = opt_onHide || null;
|
||||
// Set direction.
|
||||
div.style.direction = rtl ? 'rtl' : 'ltr';
|
||||
@@ -540,8 +542,8 @@ export function isVisible(): boolean {
|
||||
* animating.
|
||||
* @returns True if hidden.
|
||||
*/
|
||||
export function hideIfOwner(
|
||||
divOwner: Field, opt_withoutAnimation?: boolean): boolean {
|
||||
export function hideIfOwner<T>(
|
||||
divOwner: Field<T>, opt_withoutAnimation?: boolean): boolean {
|
||||
if (owner === divOwner) {
|
||||
if (opt_withoutAnimation) {
|
||||
hideWithoutAnimation();
|
||||
|
||||
@@ -45,15 +45,17 @@ import * as WidgetDiv from './widgetdiv.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
import * as Xml from './xml.js';
|
||||
|
||||
export type FieldValidator<T = unknown> = (value: T) => void;
|
||||
|
||||
/**
|
||||
* Abstract class for an editable field.
|
||||
*
|
||||
* @alias Blockly.Field
|
||||
*/
|
||||
export abstract class Field implements IASTNodeLocationSvg,
|
||||
IASTNodeLocationWithBlock,
|
||||
IKeyboardAccessible, IRegistrable {
|
||||
export abstract class Field<T = unknown> implements IASTNodeLocationSvg,
|
||||
IASTNodeLocationWithBlock,
|
||||
IKeyboardAccessible,
|
||||
IRegistrable {
|
||||
/** Non-breaking space. */
|
||||
static readonly NBSP = '\u00A0';
|
||||
|
||||
@@ -69,10 +71,10 @@ export abstract class Field implements IASTNodeLocationSvg,
|
||||
* Static labels are usually unnamed.
|
||||
*/
|
||||
name?: string = undefined;
|
||||
protected value_: AnyDuringMigration;
|
||||
protected value_: T|null;
|
||||
|
||||
/** Validation function called when user edits an editable field. */
|
||||
protected validator_: Function|null = null;
|
||||
protected validator_: FieldValidator<T>|null = null;
|
||||
|
||||
/**
|
||||
* Used to cache the field's tooltip value if setTooltip is called when the
|
||||
@@ -181,7 +183,7 @@ export abstract class Field implements IASTNodeLocationSvg,
|
||||
* this parameter supports.
|
||||
*/
|
||||
constructor(
|
||||
value: AnyDuringMigration, opt_validator?: Function|null,
|
||||
value: T|Sentinel, opt_validator?: FieldValidator<T>|null,
|
||||
opt_config?: FieldConfig) {
|
||||
/**
|
||||
* A generic value possessed by the field.
|
||||
@@ -597,7 +599,7 @@ export abstract class Field implements IASTNodeLocationSvg,
|
||||
* @param handler The validator function or null to clear a previous
|
||||
* validator.
|
||||
*/
|
||||
setValidator(handler: Function) {
|
||||
setValidator(handler: FieldValidator<T>) {
|
||||
this.validator_ = handler;
|
||||
}
|
||||
|
||||
@@ -1073,7 +1075,7 @@ export abstract class Field implements IASTNodeLocationSvg,
|
||||
}
|
||||
const gesture = (this.sourceBlock_.workspace as WorkspaceSvg).getGesture(e);
|
||||
if (gesture) {
|
||||
gesture.setStartField(this);
|
||||
gesture.setStartField(this as Field);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import * as Css from './css.js';
|
||||
import * as dropDownDiv from './dropdowndiv.js';
|
||||
import {Field, UnattachedFieldError} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {FieldTextInputConfig, FieldTextInput} from './field_textinput.js';
|
||||
import {FieldInput, FieldInputConfig, FieldInputValidator} from './field_input.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import {KeyCodes} from './utils/keycodes.js';
|
||||
import * as math from './utils/math.js';
|
||||
@@ -27,13 +27,14 @@ import {Svg} from './utils/svg.js';
|
||||
import * as userAgent from './utils/useragent.js';
|
||||
import * as WidgetDiv from './widgetdiv.js';
|
||||
|
||||
export type FieldAngleValidator = FieldInputValidator<number>;
|
||||
|
||||
/**
|
||||
* Class for an editable angle field.
|
||||
*
|
||||
* @alias Blockly.FieldAngle
|
||||
*/
|
||||
export class FieldAngle extends FieldTextInput {
|
||||
export class FieldAngle extends FieldInput<number> {
|
||||
/** The default value for this field. */
|
||||
// protected override DEFAULT_VALUE = 0;
|
||||
|
||||
@@ -135,7 +136,7 @@ export class FieldAngle extends FieldTextInput {
|
||||
* for a list of properties this parameter supports.
|
||||
*/
|
||||
constructor(
|
||||
opt_value?: string|number|Sentinel, opt_validator?: Function,
|
||||
opt_value?: string|number|Sentinel, opt_validator?: FieldAngleValidator,
|
||||
opt_config?: FieldAngleConfig) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
@@ -480,7 +481,7 @@ export class FieldAngle extends FieldTextInput {
|
||||
* @nocollapse
|
||||
* @internal
|
||||
*/
|
||||
static override fromJson(options: FieldAngleFromJsonConfig): FieldAngle {
|
||||
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);
|
||||
@@ -541,7 +542,7 @@ export enum Mode {
|
||||
/**
|
||||
* Extra configuration options for the angle field.
|
||||
*/
|
||||
export interface FieldAngleConfig extends FieldTextInputConfig {
|
||||
export interface FieldAngleConfig extends FieldInputConfig {
|
||||
mode?: Mode;
|
||||
clockwise?: boolean;
|
||||
offset?: number;
|
||||
|
||||
@@ -16,17 +16,18 @@ goog.declareModuleId('Blockly.FieldCheckbox');
|
||||
import './events/events_block_change.js';
|
||||
|
||||
import * as dom from './utils/dom.js';
|
||||
import {FieldConfig, Field} from './field.js';
|
||||
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<boolean>;
|
||||
|
||||
/**
|
||||
* Class for a checkbox field.
|
||||
*
|
||||
* @alias Blockly.FieldCheckbox
|
||||
*/
|
||||
export class FieldCheckbox extends Field {
|
||||
export class FieldCheckbox extends Field<boolean> {
|
||||
/** Default character for the checkmark. */
|
||||
static readonly CHECK_CHAR = '✓';
|
||||
private checkChar_: string;
|
||||
@@ -58,7 +59,8 @@ export class FieldCheckbox extends Field {
|
||||
* for a list of properties this parameter supports.
|
||||
*/
|
||||
constructor(
|
||||
opt_value?: string|boolean|Sentinel, opt_validator?: Function,
|
||||
opt_value?: string|boolean|Sentinel,
|
||||
opt_validator?: FieldCheckboxValidator,
|
||||
opt_config?: FieldCheckboxConfig) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ 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 {FieldConfig, Field} from './field.js';
|
||||
import {Field, FieldConfig, FieldValidator} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import * as aria from './utils/aria.js';
|
||||
import * as colour from './utils/colour.js';
|
||||
@@ -29,13 +29,14 @@ import {KeyCodes} from './utils/keycodes.js';
|
||||
import type {Sentinel} from './utils/sentinel.js';
|
||||
import {Size} from './utils/size.js';
|
||||
|
||||
export type FieldColourValidator = FieldValidator<string>;
|
||||
|
||||
/**
|
||||
* Class for a colour input field.
|
||||
*
|
||||
* @alias Blockly.FieldColour
|
||||
*/
|
||||
export class FieldColour extends Field {
|
||||
export class FieldColour extends Field<string> {
|
||||
/**
|
||||
* An array of colour strings for the palette.
|
||||
* Copied from goog.ui.ColorPicker.SIMPLE_GRID_COLORS
|
||||
@@ -152,7 +153,7 @@ export class FieldColour extends Field {
|
||||
* for a list of properties this parameter supports.
|
||||
*/
|
||||
constructor(
|
||||
opt_value?: string|Sentinel, opt_validator?: Function,
|
||||
opt_value?: string|Sentinel, opt_validator?: FieldColourValidator,
|
||||
opt_config?: FieldColourConfig) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ goog.declareModuleId('Blockly.FieldDropdown');
|
||||
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import * as dropDownDiv from './dropdowndiv.js';
|
||||
import {FieldConfig, Field, UnattachedFieldError} from './field.js';
|
||||
import {Field, FieldConfig, FieldValidator, UnattachedFieldError} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {Menu} from './menu.js';
|
||||
import {MenuItem} from './menuitem.js';
|
||||
@@ -28,12 +28,14 @@ import type {Sentinel} from './utils/sentinel.js';
|
||||
import * as utilsString from './utils/string.js';
|
||||
import {Svg} from './utils/svg.js';
|
||||
|
||||
export type FieldDropdownValidator = FieldValidator<string>;
|
||||
|
||||
/**
|
||||
* Class for an editable dropdown field.
|
||||
*
|
||||
* @alias Blockly.FieldDropdown
|
||||
*/
|
||||
export class FieldDropdown extends Field {
|
||||
export class FieldDropdown extends Field<string> {
|
||||
/** Horizontal distance that a checkmark overhangs the dropdown. */
|
||||
static CHECKMARK_OVERHANG = 25;
|
||||
|
||||
@@ -111,13 +113,13 @@ export class FieldDropdown extends Field {
|
||||
*/
|
||||
constructor(
|
||||
menuGenerator: MenuGenerator,
|
||||
opt_validator?: Function,
|
||||
opt_validator?: FieldDropdownValidator,
|
||||
opt_config?: FieldConfig,
|
||||
);
|
||||
constructor(menuGenerator: Sentinel);
|
||||
constructor(
|
||||
menuGenerator: MenuGenerator|Sentinel,
|
||||
opt_validator?: Function,
|
||||
opt_validator?: FieldDropdownValidator,
|
||||
opt_config?: FieldConfig,
|
||||
) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.FieldImage');
|
||||
|
||||
import {FieldConfig, Field} from './field.js';
|
||||
import {Field, FieldConfig} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import * as parsing from './utils/parsing.js';
|
||||
@@ -20,13 +20,12 @@ import type {Sentinel} from './utils/sentinel.js';
|
||||
import {Size} from './utils/size.js';
|
||||
import {Svg} from './utils/svg.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for an image on a block.
|
||||
*
|
||||
* @alias Blockly.FieldImage
|
||||
*/
|
||||
export class FieldImage extends Field {
|
||||
export class FieldImage extends Field<string> {
|
||||
/**
|
||||
* Vertical padding below the image, which is included in the reported height
|
||||
* of the field.
|
||||
|
||||
600
core/field_input.ts
Normal file
600
core/field_input.ts
Normal file
@@ -0,0 +1,600 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2012 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Text input field.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.FieldInput');
|
||||
|
||||
// Unused import preserved for side-effects. Remove if unneeded.
|
||||
import './events/events_block_change.js';
|
||||
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import * as dialog from './dialog.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import * as dropDownDiv from './dropdowndiv.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {Field, FieldConfig, FieldValidator, UnattachedFieldError} from './field.js';
|
||||
import {Msg} from './msg.js';
|
||||
import * as aria from './utils/aria.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import {KeyCodes} from './utils/keycodes.js';
|
||||
import type {Sentinel} from './utils/sentinel.js';
|
||||
import * as userAgent from './utils/useragent.js';
|
||||
import * as WidgetDiv from './widgetdiv.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
export type InputTypes = string|number;
|
||||
export type FieldInputValidator<T extends InputTypes> = FieldValidator<T>;
|
||||
|
||||
/**
|
||||
* Class for an editable text field.
|
||||
*
|
||||
* @alias Blockly.FieldInput
|
||||
*/
|
||||
export abstract class FieldInput<T extends InputTypes> extends Field<T> {
|
||||
/**
|
||||
* Pixel size of input border radius.
|
||||
* Should match blocklyText's border-radius in CSS.
|
||||
*/
|
||||
static BORDERRADIUS = 4;
|
||||
|
||||
/** Allow browser to spellcheck this field. */
|
||||
protected spellcheck_ = true;
|
||||
|
||||
/** The HTML input element. */
|
||||
protected htmlInput_: HTMLInputElement|null = null;
|
||||
|
||||
/** True if the field's value is currently being edited via the UI. */
|
||||
protected isBeingEdited_ = false;
|
||||
|
||||
/**
|
||||
* True if the value currently displayed in the field's editory UI is valid.
|
||||
*/
|
||||
protected isTextValid_ = false;
|
||||
|
||||
/** Key down event data. */
|
||||
private onKeyDownWrapper_: browserEvents.Data|null = null;
|
||||
|
||||
/** Key input event data. */
|
||||
private onKeyInputWrapper_: browserEvents.Data|null = null;
|
||||
|
||||
/**
|
||||
* Whether the field should consider the whole parent block to be its click
|
||||
* target.
|
||||
*/
|
||||
fullBlockClickTarget_: boolean|null = false;
|
||||
|
||||
/** The workspace that this field belongs to. */
|
||||
protected workspace_: WorkspaceSvg|null = null;
|
||||
|
||||
/**
|
||||
* 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 = 'text';
|
||||
override clickTarget_: AnyDuringMigration;
|
||||
override value_: AnyDuringMigration;
|
||||
override isDirty_: AnyDuringMigration;
|
||||
|
||||
/**
|
||||
* @param opt_value The initial value 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 opt_validator A function that is called to validate changes to the
|
||||
* field's value. Takes in a string & returns a validated string, or null
|
||||
* to abort the change.
|
||||
* @param opt_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/text-input#creation}
|
||||
* for a list of properties this parameter supports.
|
||||
*/
|
||||
constructor(
|
||||
opt_value?: string|Sentinel, opt_validator?: FieldInputValidator<T>|null,
|
||||
opt_config?: FieldInputConfig) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
if (opt_value === Field.SKIP_SETUP) {
|
||||
return;
|
||||
}
|
||||
if (opt_config) {
|
||||
this.configure_(opt_config);
|
||||
}
|
||||
this.setValue(opt_value);
|
||||
if (opt_validator) {
|
||||
this.setValidator(opt_validator);
|
||||
}
|
||||
}
|
||||
|
||||
protected override configure_(config: FieldInputConfig) {
|
||||
super.configure_(config);
|
||||
if (config.spellcheck !== undefined) {
|
||||
this.spellcheck_ = config.spellcheck;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override initView() {
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
}
|
||||
if (this.getConstants()!.FULL_BLOCK_FIELDS) {
|
||||
// Step one: figure out if this is the only field on this block.
|
||||
// Rendering is quite different in that case.
|
||||
let nFields = 0;
|
||||
let nConnections = 0;
|
||||
// Count the number of fields, excluding text fields
|
||||
for (let i = 0, input; input = block.inputList[i]; i++) {
|
||||
for (let j = 0; input.fieldRow[j]; j++) {
|
||||
nFields++;
|
||||
}
|
||||
if (input.connection) {
|
||||
nConnections++;
|
||||
}
|
||||
}
|
||||
// The special case is when this is the only non-label field on the block
|
||||
// and it has an output but no inputs.
|
||||
this.fullBlockClickTarget_ =
|
||||
nFields <= 1 && block.outputConnection && !nConnections;
|
||||
} else {
|
||||
this.fullBlockClickTarget_ = false;
|
||||
}
|
||||
|
||||
if (this.fullBlockClickTarget_) {
|
||||
this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot();
|
||||
} else {
|
||||
this.createBorderRect_();
|
||||
}
|
||||
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
|
||||
* 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_.
|
||||
*/
|
||||
protected override doValueInvalid_(_invalidValue: AnyDuringMigration) {
|
||||
if (this.isBeingEdited_) {
|
||||
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()) {
|
||||
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
|
||||
this.sourceBlock_, 'field', this.name || null, oldValue,
|
||||
this.value_));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_).
|
||||
*
|
||||
* @param newValue The value to be saved. The default validator guarantees
|
||||
* that this is a string.
|
||||
*/
|
||||
protected override doValueUpdate_(newValue: AnyDuringMigration) {
|
||||
this.isTextValid_ = true;
|
||||
this.value_ = newValue;
|
||||
if (!this.isBeingEdited_) {
|
||||
// This should only occur if setValue is triggered programmatically.
|
||||
this.isDirty_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates text field to match the colour/style of the block.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
override applyColour() {
|
||||
if (!this.sourceBlock_ || !this.getConstants()!.FULL_BLOCK_FIELDS) return;
|
||||
|
||||
const source = this.sourceBlock_ as BlockSvg;
|
||||
|
||||
if (this.borderRect_) {
|
||||
this.borderRect_.setAttribute('stroke', source.style.colourTertiary);
|
||||
} else {
|
||||
source.pathObject.svgPath.setAttribute(
|
||||
'fill', this.getConstants()!.FIELD_BORDER_RECT_COLOUR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the colour of the htmlInput given the current validity of the
|
||||
* field's value.
|
||||
*/
|
||||
protected override render_() {
|
||||
super.render_();
|
||||
// This logic is done in render_ rather than doValueInvalid_ or
|
||||
// doValueUpdate_ so that the code is more centralized.
|
||||
if (this.isBeingEdited_) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether this field is spellchecked by the browser.
|
||||
*
|
||||
* @param check True if checked.
|
||||
*/
|
||||
setSpellcheck(check: boolean) {
|
||||
if (check === this.spellcheck_) {
|
||||
return;
|
||||
}
|
||||
this.spellcheck_ = check;
|
||||
if (this.htmlInput_) {
|
||||
// AnyDuringMigration because: Argument of type 'boolean' is not
|
||||
// assignable to parameter of type 'string'.
|
||||
this.htmlInput_.setAttribute(
|
||||
'spellcheck', this.spellcheck_ as AnyDuringMigration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the inline free-text editor on top of the text.
|
||||
*
|
||||
* @param _opt_e Optional mouse event that triggered the field to open, or
|
||||
* undefined if triggered programmatically.
|
||||
* @param opt_quietInput True if editor should be created without focus.
|
||||
* Defaults to false.
|
||||
*/
|
||||
protected override showEditor_(_opt_e?: Event, opt_quietInput?: boolean) {
|
||||
this.workspace_ = (this.sourceBlock_ as BlockSvg).workspace;
|
||||
const quietInput = opt_quietInput || false;
|
||||
if (!quietInput &&
|
||||
(userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD)) {
|
||||
this.showPromptEditor_();
|
||||
} else {
|
||||
this.showInlineEditor_(quietInput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show a text input editor that is a prompt (usually a popup).
|
||||
* Mobile browsers have issues with in-line textareas (focus and keyboards).
|
||||
*/
|
||||
private showPromptEditor_() {
|
||||
dialog.prompt(
|
||||
Msg['CHANGE_VALUE_TITLE'], this.getText(), (text: string|null) => {
|
||||
// Text is null if user pressed cancel button.
|
||||
if (text !== null) {
|
||||
this.setValue(this.getValueFromEditorText_(text));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show a text input editor that sits directly over the text input.
|
||||
*
|
||||
* @param quietInput True if editor should be created without focus.
|
||||
*/
|
||||
private showInlineEditor_(quietInput: boolean) {
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
}
|
||||
WidgetDiv.show(this, block.RTL, this.widgetDispose_.bind(this));
|
||||
this.htmlInput_ = this.widgetCreate_() as HTMLInputElement;
|
||||
this.isBeingEdited_ = true;
|
||||
|
||||
if (!quietInput) {
|
||||
(this.htmlInput_ as HTMLElement).focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
this.htmlInput_.select();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the text input editor widget.
|
||||
*
|
||||
* @returns The newly created text input editor.
|
||||
*/
|
||||
protected widgetCreate_(): HTMLElement {
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
}
|
||||
eventUtils.setGroup(true);
|
||||
const div = WidgetDiv.getDiv();
|
||||
|
||||
const clickTarget = this.getClickTarget_();
|
||||
if (!clickTarget) throw new Error('A click target has not been set.');
|
||||
dom.addClass(clickTarget, 'editing');
|
||||
|
||||
const htmlInput = (document.createElement('input'));
|
||||
htmlInput.className = 'blocklyHtmlInput';
|
||||
// AnyDuringMigration because: Argument of type 'boolean' is not assignable
|
||||
// to parameter of type 'string'.
|
||||
htmlInput.setAttribute(
|
||||
'spellcheck', this.spellcheck_ as AnyDuringMigration);
|
||||
const scale = this.workspace_!.getScale();
|
||||
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt';
|
||||
div!.style.fontSize = fontSize;
|
||||
htmlInput.style.fontSize = fontSize;
|
||||
let borderRadius = FieldInput.BORDERRADIUS * scale + 'px';
|
||||
|
||||
if (this.fullBlockClickTarget_) {
|
||||
const bBox = this.getScaledBBox();
|
||||
|
||||
// Override border radius.
|
||||
borderRadius = (bBox.bottom - bBox.top) / 2 + 'px';
|
||||
// Pull stroke colour from the existing shadow block
|
||||
const strokeColour = block.getParent() ?
|
||||
(block.getParent() as BlockSvg).style.colourTertiary :
|
||||
(this.sourceBlock_ as BlockSvg).style.colourTertiary;
|
||||
htmlInput.style.border = 1 * scale + 'px solid ' + strokeColour;
|
||||
div!.style.borderRadius = borderRadius;
|
||||
div!.style.transition = 'box-shadow 0.25s ease 0s';
|
||||
if (this.getConstants()!.FIELD_TEXTINPUT_BOX_SHADOW) {
|
||||
div!.style.boxShadow =
|
||||
'rgba(255, 255, 255, 0.3) 0 0 0 ' + 4 * scale + 'px';
|
||||
}
|
||||
}
|
||||
htmlInput.style.borderRadius = borderRadius;
|
||||
|
||||
div!.appendChild(htmlInput);
|
||||
|
||||
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
|
||||
htmlInput.setAttribute('data-untyped-default-value', this.value_);
|
||||
htmlInput.setAttribute('data-old-value', '');
|
||||
|
||||
this.resizeEditor_();
|
||||
|
||||
this.bindInputEvents_(htmlInput);
|
||||
|
||||
return htmlInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the editor, saves the results, and disposes of any events or
|
||||
* DOM-references belonging to the editor.
|
||||
*/
|
||||
protected widgetDispose_() {
|
||||
// Non-disposal related things that we do when the editor closes.
|
||||
this.isBeingEdited_ = false;
|
||||
this.isTextValid_ = true;
|
||||
// Make sure the field's node matches the field's internal value.
|
||||
this.forceRerender();
|
||||
this.onFinishEditing_(this.value_);
|
||||
eventUtils.setGroup(false);
|
||||
|
||||
// Actual disposal.
|
||||
this.unbindInputEvents_();
|
||||
const style = WidgetDiv.getDiv()!.style;
|
||||
style.width = 'auto';
|
||||
style.height = 'auto';
|
||||
style.fontSize = '';
|
||||
style.transition = '';
|
||||
style.boxShadow = '';
|
||||
this.htmlInput_ = null;
|
||||
|
||||
const clickTarget = this.getClickTarget_();
|
||||
if (!clickTarget) throw new Error('A click target has not been set.');
|
||||
dom.removeClass(clickTarget, 'editing');
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback triggered when the user is done editing the field via the UI.
|
||||
*
|
||||
* @param _value The new value of the field.
|
||||
*/
|
||||
onFinishEditing_(_value: AnyDuringMigration) {}
|
||||
// NOP by default.
|
||||
// TODO(#2496): Support people passing a func into the field.
|
||||
|
||||
/**
|
||||
* Bind handlers for user input on the text input field's editor.
|
||||
*
|
||||
* @param htmlInput The htmlInput to which event handlers will be bound.
|
||||
*/
|
||||
protected bindInputEvents_(htmlInput: HTMLElement) {
|
||||
// Trap Enter without IME and Esc to hide.
|
||||
this.onKeyDownWrapper_ = browserEvents.conditionalBind(
|
||||
htmlInput, 'keydown', this, this.onHtmlInputKeyDown_);
|
||||
// Resize after every input change.
|
||||
this.onKeyInputWrapper_ = browserEvents.conditionalBind(
|
||||
htmlInput, 'input', this, this.onHtmlInputChange_);
|
||||
}
|
||||
|
||||
/** Unbind handlers for user input and workspace size changes. */
|
||||
protected unbindInputEvents_() {
|
||||
if (this.onKeyDownWrapper_) {
|
||||
browserEvents.unbind(this.onKeyDownWrapper_);
|
||||
this.onKeyDownWrapper_ = null;
|
||||
}
|
||||
if (this.onKeyInputWrapper_) {
|
||||
browserEvents.unbind(this.onKeyInputWrapper_);
|
||||
this.onKeyInputWrapper_ = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key down to the editor.
|
||||
*
|
||||
* @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) {
|
||||
WidgetDiv.hide();
|
||||
dropDownDiv.hideWithoutAnimation();
|
||||
// AnyDuringMigration because: Property 'keyCode' does not exist on type
|
||||
// 'Event'.
|
||||
} else if ((e as AnyDuringMigration).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) {
|
||||
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);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a change to the editor.
|
||||
*
|
||||
* @param _e Keyboard event.
|
||||
*/
|
||||
private onHtmlInputChange_(_e: Event) {
|
||||
const text = this.htmlInput_!.value;
|
||||
if (text !== this.htmlInput_!.getAttribute('data-old-value')) {
|
||||
this.htmlInput_!.setAttribute('data-old-value', text);
|
||||
|
||||
const value = this.getValueFromEditorText_(text);
|
||||
this.setValue(value);
|
||||
this.forceRerender();
|
||||
this.resizeEditor_();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTML input value and the field's internal value. The difference
|
||||
* between this and `setValue` is that this also updates the HTML input
|
||||
* value whilst editing.
|
||||
*
|
||||
* @param newValue New value.
|
||||
*/
|
||||
protected setEditorValue_(newValue: AnyDuringMigration) {
|
||||
this.isDirty_ = true;
|
||||
if (this.isBeingEdited_) {
|
||||
// In the case this method is passed an invalid value, we still
|
||||
// pass it through the transformation method `getEditorText` to deal
|
||||
// with. Otherwise, the internal field's state will be inconsistent
|
||||
// with what's shown to the user.
|
||||
this.htmlInput_!.value = this.getEditorText_(newValue);
|
||||
}
|
||||
this.setValue(newValue);
|
||||
}
|
||||
|
||||
/** Resize the editor to fit the text. */
|
||||
protected resizeEditor_() {
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
}
|
||||
const div = WidgetDiv.getDiv();
|
||||
const bBox = this.getScaledBBox();
|
||||
div!.style.width = bBox.right - bBox.left + 'px';
|
||||
div!.style.height = bBox.bottom - bBox.top + 'px';
|
||||
|
||||
// In RTL mode block fields and LTR input fields the left edge moves,
|
||||
// whereas the right edge is fixed. Reposition the editor.
|
||||
const x = block.RTL ? bBox.right - div!.offsetWidth : bBox.left;
|
||||
const xy = new Coordinate(x, bBox.top);
|
||||
|
||||
div!.style.left = xy.x + 'px';
|
||||
div!.style.top = xy.y + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the field is tab navigable.
|
||||
*
|
||||
* @returns True if the field is tab navigable.
|
||||
*/
|
||||
override isTabNavigable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the `getText_` developer hook to override the field's text
|
||||
* representation. When we're currently editing, return the current HTML value
|
||||
* instead. Otherwise, return null which tells the field to use the default
|
||||
* behaviour (which is a string cast of the field's value).
|
||||
*
|
||||
* @returns The HTML value if we're editing, otherwise null.
|
||||
*/
|
||||
protected override getText_(): string|null {
|
||||
if (this.isBeingEdited_ && this.htmlInput_) {
|
||||
// We are currently editing, return the HTML input value instead.
|
||||
return this.htmlInput_.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the provided value into a text to show in the HTML input.
|
||||
* Override this method if the field's HTML input representation is different
|
||||
* than the field's value. This should be coupled with an override of
|
||||
* `getValueFromEditorText_`.
|
||||
*
|
||||
* @param value The value stored in this field.
|
||||
* @returns The text to show on the HTML input.
|
||||
*/
|
||||
protected getEditorText_(value: AnyDuringMigration): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the text received from the HTML input into a value to store
|
||||
* in this field.
|
||||
* Override this method if the field's HTML input representation is different
|
||||
* than the field's value. This should be coupled with an override of
|
||||
* `getEditorText_`.
|
||||
*
|
||||
* @param text Text received from the HTML input.
|
||||
* @returns The value to store.
|
||||
*/
|
||||
protected getValueFromEditorText_(text: string): AnyDuringMigration {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Config options for the input field.
|
||||
*/
|
||||
export interface FieldInputConfig extends FieldConfig {
|
||||
spellcheck?: boolean;
|
||||
}
|
||||
@@ -14,18 +14,17 @@ import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.FieldLabel');
|
||||
|
||||
import * as dom from './utils/dom.js';
|
||||
import {FieldConfig, Field} from './field.js';
|
||||
import {Field, FieldConfig} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import * as parsing from './utils/parsing.js';
|
||||
import type {Sentinel} from './utils/sentinel.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for a non-editable, non-serializable text field.
|
||||
*
|
||||
* @alias Blockly.FieldLabel
|
||||
*/
|
||||
export class FieldLabel extends Field {
|
||||
export class FieldLabel extends Field<string> {
|
||||
/** The html class name to use for this field. */
|
||||
private class_: string|null = null;
|
||||
|
||||
|
||||
@@ -14,11 +14,10 @@
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.FieldLabelSerializable');
|
||||
|
||||
import {FieldLabelConfig, FieldLabelFromJsonConfig, FieldLabel} from './field_label.js';
|
||||
import {FieldLabel, FieldLabelConfig, FieldLabelFromJsonConfig} from './field_label.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import * as parsing from './utils/parsing.js';
|
||||
|
||||
|
||||
/**
|
||||
* Class for a non-editable, serializable text field.
|
||||
*
|
||||
|
||||
@@ -15,7 +15,7 @@ goog.declareModuleId('Blockly.FieldMultilineInput');
|
||||
import * as Css from './css.js';
|
||||
import {Field, UnattachedFieldError} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {FieldTextInputConfig, FieldTextInput} from './field_textinput.js';
|
||||
import {FieldTextInput, FieldTextInputConfig, FieldTextInputValidator} from './field_textinput.js';
|
||||
import * as aria from './utils/aria.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import {KeyCodes} from './utils/keycodes.js';
|
||||
@@ -25,6 +25,7 @@ import {Svg} from './utils/svg.js';
|
||||
import * as userAgent from './utils/useragent.js';
|
||||
import * as WidgetDiv from './widgetdiv.js';
|
||||
|
||||
export type FieldMultilineInputValidator = FieldTextInputValidator;
|
||||
|
||||
/**
|
||||
* Class for an editable text area field.
|
||||
@@ -65,7 +66,7 @@ export class FieldMultilineInput extends FieldTextInput {
|
||||
* for a list of properties this parameter supports.
|
||||
*/
|
||||
constructor(
|
||||
opt_value?: string|Sentinel, opt_validator?: Function,
|
||||
opt_value?: string|Sentinel, opt_validator?: FieldMultilineInputValidator,
|
||||
opt_config?: FieldMultilineInputConfig) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
|
||||
@@ -14,17 +14,18 @@ goog.declareModuleId('Blockly.FieldNumber');
|
||||
|
||||
import {Field} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {FieldTextInputConfig, FieldTextInput} from './field_textinput.js';
|
||||
import {FieldInput, FieldInputConfig, FieldInputValidator} from './field_input.js';
|
||||
import * as aria from './utils/aria.js';
|
||||
import type {Sentinel} from './utils/sentinel.js';
|
||||
|
||||
export type FieldNumberValidator = FieldInputValidator<number>;
|
||||
|
||||
/**
|
||||
* Class for an editable number field.
|
||||
*
|
||||
* @alias Blockly.FieldNumber
|
||||
*/
|
||||
export class FieldNumber extends FieldTextInput {
|
||||
export class FieldNumber extends FieldInput<number> {
|
||||
/** The minimum value this number field can contain. */
|
||||
protected min_ = -Infinity;
|
||||
|
||||
@@ -68,7 +69,8 @@ export class FieldNumber extends FieldTextInput {
|
||||
constructor(
|
||||
opt_value?: string|number|Sentinel, opt_min?: string|number|null,
|
||||
opt_max?: string|number|null, opt_precision?: string|number|null,
|
||||
opt_validator?: Function|null, opt_config?: FieldNumberConfig) {
|
||||
opt_validator?: FieldNumberValidator|null,
|
||||
opt_config?: FieldNumberConfig) {
|
||||
// Pass SENTINEL so that we can define properties before value validation.
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
@@ -310,7 +312,7 @@ export class FieldNumber extends FieldTextInput {
|
||||
* @nocollapse
|
||||
* @internal
|
||||
*/
|
||||
static override fromJson(options: FieldNumberFromJsonConfig): FieldNumber {
|
||||
static fromJson(options: FieldNumberFromJsonConfig): FieldNumber {
|
||||
// `this` might be a subclass of FieldNumber if that class doesn't override
|
||||
// the static fromJson method.
|
||||
return new this(
|
||||
@@ -325,7 +327,7 @@ fieldRegistry.register('field_number', FieldNumber);
|
||||
/**
|
||||
* Config options for the number field.
|
||||
*/
|
||||
export interface FieldNumberConfig extends FieldTextInputConfig {
|
||||
export interface FieldNumberConfig extends FieldInputConfig {
|
||||
min?: number;
|
||||
max?: number;
|
||||
precision?: number;
|
||||
|
||||
@@ -14,10 +14,13 @@
|
||||
import * as goog from '../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.fieldRegistry');
|
||||
|
||||
import type {Field} from './field.js';
|
||||
import type {IRegistrableField} from './interfaces/i_registrable_field.js';
|
||||
import type {Field, FieldProto} from './field.js';
|
||||
import * as registry from './registry.js';
|
||||
|
||||
interface RegistryOptions {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a field type.
|
||||
@@ -31,7 +34,7 @@ import * as registry from './registry.js';
|
||||
* or the fieldClass is not an object containing a fromJson function.
|
||||
* @alias Blockly.fieldRegistry.register
|
||||
*/
|
||||
export function register(type: string, fieldClass: IRegistrableField) {
|
||||
export function register(type: string, fieldClass: FieldProto) {
|
||||
registry.register(registry.Type.FIELD, type, fieldClass);
|
||||
}
|
||||
|
||||
@@ -57,7 +60,7 @@ export function unregister(type: string) {
|
||||
* @alias Blockly.fieldRegistry.fromJson
|
||||
* @internal
|
||||
*/
|
||||
export function fromJson(options: AnyDuringMigration): Field|null {
|
||||
export function fromJson<T>(options: RegistryOptions): Field<T>|null {
|
||||
return TEST_ONLY.fromJsonInternal(options);
|
||||
}
|
||||
|
||||
@@ -66,8 +69,8 @@ export function fromJson(options: AnyDuringMigration): Field|null {
|
||||
*
|
||||
* @param options
|
||||
*/
|
||||
function fromJsonInternal(options: AnyDuringMigration): Field|null {
|
||||
const fieldObject = registry.getObject(registry.Type.FIELD, options['type']);
|
||||
function fromJsonInternal<T>(options: RegistryOptions): Field<T>|null {
|
||||
const fieldObject = registry.getObject(registry.Type.FIELD, options.type);
|
||||
if (!fieldObject) {
|
||||
console.warn(
|
||||
'Blockly could not create a field of type ' + options['type'] +
|
||||
@@ -78,7 +81,8 @@ function fromJsonInternal(options: AnyDuringMigration): Field|null {
|
||||
} else if (typeof (fieldObject as any).fromJson !== 'function') {
|
||||
throw new TypeError('returned Field was not a IRegistrableField');
|
||||
} else {
|
||||
return (fieldObject as unknown as IRegistrableField).fromJson(options);
|
||||
type fromJson = (options: {}) => Field<T>;
|
||||
return (fieldObject as unknown as {fromJson: fromJson}).fromJson(options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,78 +15,14 @@ goog.declareModuleId('Blockly.FieldTextInput');
|
||||
// Unused import preserved for side-effects. Remove if unneeded.
|
||||
import './events/events_block_change.js';
|
||||
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import * as dialog from './dialog.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import * as dropDownDiv from './dropdowndiv.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {FieldConfig, Field, UnattachedFieldError} from './field.js';
|
||||
import {FieldInput, FieldInputConfig, FieldInputValidator} from './field_input.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {Msg} from './msg.js';
|
||||
import * as aria from './utils/aria.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import {KeyCodes} from './utils/keycodes.js';
|
||||
import * as parsing from './utils/parsing.js';
|
||||
import type {Sentinel} from './utils/sentinel.js';
|
||||
import * as userAgent from './utils/useragent.js';
|
||||
import * as WidgetDiv from './widgetdiv.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
export type FieldTextInputValidator = FieldInputValidator<string>;
|
||||
|
||||
/**
|
||||
* Class for an editable text field.
|
||||
*
|
||||
* @alias Blockly.FieldTextInput
|
||||
*/
|
||||
export class FieldTextInput extends Field {
|
||||
/**
|
||||
* Pixel size of input border radius.
|
||||
* Should match blocklyText's border-radius in CSS.
|
||||
*/
|
||||
static BORDERRADIUS = 4;
|
||||
|
||||
/** Allow browser to spellcheck this field. */
|
||||
protected spellcheck_ = true;
|
||||
|
||||
/** The HTML input element. */
|
||||
protected htmlInput_: HTMLInputElement|null = null;
|
||||
|
||||
/** True if the field's value is currently being edited via the UI. */
|
||||
protected isBeingEdited_ = false;
|
||||
|
||||
/**
|
||||
* True if the value currently displayed in the field's editory UI is valid.
|
||||
*/
|
||||
protected isTextValid_ = false;
|
||||
|
||||
/** Key down event data. */
|
||||
private onKeyDownWrapper_: browserEvents.Data|null = null;
|
||||
|
||||
/** Key input event data. */
|
||||
private onKeyInputWrapper_: browserEvents.Data|null = null;
|
||||
|
||||
/**
|
||||
* Whether the field should consider the whole parent block to be its click
|
||||
* target.
|
||||
*/
|
||||
fullBlockClickTarget_: boolean|null = false;
|
||||
|
||||
/** The workspace that this field belongs to. */
|
||||
protected workspace_: WorkspaceSvg|null = null;
|
||||
|
||||
/**
|
||||
* 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 = 'text';
|
||||
override clickTarget_: AnyDuringMigration;
|
||||
override value_: AnyDuringMigration;
|
||||
override isDirty_: AnyDuringMigration;
|
||||
|
||||
export class FieldTextInput extends FieldInput<string> {
|
||||
/**
|
||||
* @param opt_value The initial value of the field. Should cast to a string.
|
||||
* Defaults to an empty string if null or undefined. Also accepts
|
||||
@@ -102,493 +38,9 @@ export class FieldTextInput extends Field {
|
||||
* for a list of properties this parameter supports.
|
||||
*/
|
||||
constructor(
|
||||
opt_value?: string|Sentinel, opt_validator?: Function|null,
|
||||
opt_config?: FieldTextInputConfig) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
if (opt_value === Field.SKIP_SETUP) {
|
||||
return;
|
||||
}
|
||||
if (opt_config) {
|
||||
this.configure_(opt_config);
|
||||
}
|
||||
this.setValue(opt_value);
|
||||
if (opt_validator) {
|
||||
this.setValidator(opt_validator);
|
||||
}
|
||||
}
|
||||
|
||||
protected override configure_(config: FieldTextInputConfig) {
|
||||
super.configure_(config);
|
||||
if (config.spellcheck !== undefined) {
|
||||
this.spellcheck_ = config.spellcheck;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override initView() {
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
}
|
||||
if (this.getConstants()!.FULL_BLOCK_FIELDS) {
|
||||
// Step one: figure out if this is the only field on this block.
|
||||
// Rendering is quite different in that case.
|
||||
let nFields = 0;
|
||||
let nConnections = 0;
|
||||
// Count the number of fields, excluding text fields
|
||||
for (let i = 0, input; input = block.inputList[i]; i++) {
|
||||
for (let j = 0; input.fieldRow[j]; j++) {
|
||||
nFields++;
|
||||
}
|
||||
if (input.connection) {
|
||||
nConnections++;
|
||||
}
|
||||
}
|
||||
// The special case is when this is the only non-label field on the block
|
||||
// and it has an output but no inputs.
|
||||
this.fullBlockClickTarget_ =
|
||||
nFields <= 1 && block.outputConnection && !nConnections;
|
||||
} else {
|
||||
this.fullBlockClickTarget_ = false;
|
||||
}
|
||||
|
||||
if (this.fullBlockClickTarget_) {
|
||||
this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot();
|
||||
} else {
|
||||
this.createBorderRect_();
|
||||
}
|
||||
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
|
||||
* 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_.
|
||||
*/
|
||||
protected override doValueInvalid_(_invalidValue: AnyDuringMigration) {
|
||||
if (this.isBeingEdited_) {
|
||||
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()) {
|
||||
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
|
||||
this.sourceBlock_, 'field', this.name || null, oldValue,
|
||||
this.value_));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_).
|
||||
*
|
||||
* @param newValue The value to be saved. The default validator guarantees
|
||||
* that this is a string.
|
||||
*/
|
||||
protected override doValueUpdate_(newValue: AnyDuringMigration) {
|
||||
this.isTextValid_ = true;
|
||||
this.value_ = newValue;
|
||||
if (!this.isBeingEdited_) {
|
||||
// This should only occur if setValue is triggered programmatically.
|
||||
this.isDirty_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates text field to match the colour/style of the block.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
override applyColour() {
|
||||
if (!this.sourceBlock_ || !this.getConstants()!.FULL_BLOCK_FIELDS) return;
|
||||
|
||||
const source = this.sourceBlock_ as BlockSvg;
|
||||
|
||||
if (this.borderRect_) {
|
||||
this.borderRect_.setAttribute('stroke', source.style.colourTertiary);
|
||||
} else {
|
||||
source.pathObject.svgPath.setAttribute(
|
||||
'fill', this.getConstants()!.FIELD_BORDER_RECT_COLOUR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the colour of the htmlInput given the current validity of the
|
||||
* field's value.
|
||||
*/
|
||||
protected override render_() {
|
||||
super.render_();
|
||||
// This logic is done in render_ rather than doValueInvalid_ or
|
||||
// doValueUpdate_ so that the code is more centralized.
|
||||
if (this.isBeingEdited_) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether this field is spellchecked by the browser.
|
||||
*
|
||||
* @param check True if checked.
|
||||
*/
|
||||
setSpellcheck(check: boolean) {
|
||||
if (check === this.spellcheck_) {
|
||||
return;
|
||||
}
|
||||
this.spellcheck_ = check;
|
||||
if (this.htmlInput_) {
|
||||
// AnyDuringMigration because: Argument of type 'boolean' is not
|
||||
// assignable to parameter of type 'string'.
|
||||
this.htmlInput_.setAttribute(
|
||||
'spellcheck', this.spellcheck_ as AnyDuringMigration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the inline free-text editor on top of the text.
|
||||
*
|
||||
* @param _opt_e Optional mouse event that triggered the field to open, or
|
||||
* undefined if triggered programmatically.
|
||||
* @param opt_quietInput True if editor should be created without focus.
|
||||
* Defaults to false.
|
||||
*/
|
||||
protected override showEditor_(_opt_e?: Event, opt_quietInput?: boolean) {
|
||||
this.workspace_ = (this.sourceBlock_ as BlockSvg).workspace;
|
||||
const quietInput = opt_quietInput || false;
|
||||
if (!quietInput &&
|
||||
(userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD)) {
|
||||
this.showPromptEditor_();
|
||||
} else {
|
||||
this.showInlineEditor_(quietInput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show a text input editor that is a prompt (usually a popup).
|
||||
* Mobile browsers have issues with in-line textareas (focus and keyboards).
|
||||
*/
|
||||
private showPromptEditor_() {
|
||||
dialog.prompt(
|
||||
Msg['CHANGE_VALUE_TITLE'], this.getText(), (text: string|null) => {
|
||||
// Text is null if user pressed cancel button.
|
||||
if (text !== null) {
|
||||
this.setValue(this.getValueFromEditorText_(text));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show a text input editor that sits directly over the text input.
|
||||
*
|
||||
* @param quietInput True if editor should be created without focus.
|
||||
*/
|
||||
private showInlineEditor_(quietInput: boolean) {
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
}
|
||||
WidgetDiv.show(this, block.RTL, this.widgetDispose_.bind(this));
|
||||
this.htmlInput_ = this.widgetCreate_() as HTMLInputElement;
|
||||
this.isBeingEdited_ = true;
|
||||
|
||||
if (!quietInput) {
|
||||
(this.htmlInput_ as HTMLElement).focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
this.htmlInput_.select();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the text input editor widget.
|
||||
*
|
||||
* @returns The newly created text input editor.
|
||||
*/
|
||||
protected widgetCreate_(): HTMLElement {
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
}
|
||||
eventUtils.setGroup(true);
|
||||
const div = WidgetDiv.getDiv();
|
||||
|
||||
const clickTarget = this.getClickTarget_();
|
||||
if (!clickTarget) throw new Error('A click target has not been set.');
|
||||
dom.addClass(clickTarget, 'editing');
|
||||
|
||||
const htmlInput = (document.createElement('input'));
|
||||
htmlInput.className = 'blocklyHtmlInput';
|
||||
// AnyDuringMigration because: Argument of type 'boolean' is not assignable
|
||||
// to parameter of type 'string'.
|
||||
htmlInput.setAttribute(
|
||||
'spellcheck', this.spellcheck_ as AnyDuringMigration);
|
||||
const scale = this.workspace_!.getScale();
|
||||
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt';
|
||||
div!.style.fontSize = fontSize;
|
||||
htmlInput.style.fontSize = fontSize;
|
||||
let borderRadius = FieldTextInput.BORDERRADIUS * scale + 'px';
|
||||
|
||||
if (this.fullBlockClickTarget_) {
|
||||
const bBox = this.getScaledBBox();
|
||||
|
||||
// Override border radius.
|
||||
borderRadius = (bBox.bottom - bBox.top) / 2 + 'px';
|
||||
// Pull stroke colour from the existing shadow block
|
||||
const strokeColour = block.getParent() ?
|
||||
(block.getParent() as BlockSvg).style.colourTertiary :
|
||||
(this.sourceBlock_ as BlockSvg).style.colourTertiary;
|
||||
htmlInput.style.border = 1 * scale + 'px solid ' + strokeColour;
|
||||
div!.style.borderRadius = borderRadius;
|
||||
div!.style.transition = 'box-shadow 0.25s ease 0s';
|
||||
if (this.getConstants()!.FIELD_TEXTINPUT_BOX_SHADOW) {
|
||||
div!.style.boxShadow =
|
||||
'rgba(255, 255, 255, 0.3) 0 0 0 ' + 4 * scale + 'px';
|
||||
}
|
||||
}
|
||||
htmlInput.style.borderRadius = borderRadius;
|
||||
|
||||
div!.appendChild(htmlInput);
|
||||
|
||||
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
|
||||
htmlInput.setAttribute('data-untyped-default-value', this.value_);
|
||||
htmlInput.setAttribute('data-old-value', '');
|
||||
|
||||
this.resizeEditor_();
|
||||
|
||||
this.bindInputEvents_(htmlInput);
|
||||
|
||||
return htmlInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the editor, saves the results, and disposes of any events or
|
||||
* DOM-references belonging to the editor.
|
||||
*/
|
||||
protected widgetDispose_() {
|
||||
// Non-disposal related things that we do when the editor closes.
|
||||
this.isBeingEdited_ = false;
|
||||
this.isTextValid_ = true;
|
||||
// Make sure the field's node matches the field's internal value.
|
||||
this.forceRerender();
|
||||
this.onFinishEditing_(this.value_);
|
||||
eventUtils.setGroup(false);
|
||||
|
||||
// Actual disposal.
|
||||
this.unbindInputEvents_();
|
||||
const style = WidgetDiv.getDiv()!.style;
|
||||
style.width = 'auto';
|
||||
style.height = 'auto';
|
||||
style.fontSize = '';
|
||||
style.transition = '';
|
||||
style.boxShadow = '';
|
||||
this.htmlInput_ = null;
|
||||
|
||||
const clickTarget = this.getClickTarget_();
|
||||
if (!clickTarget) throw new Error('A click target has not been set.');
|
||||
dom.removeClass(clickTarget, 'editing');
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback triggered when the user is done editing the field via the UI.
|
||||
*
|
||||
* @param _value The new value of the field.
|
||||
*/
|
||||
onFinishEditing_(_value: AnyDuringMigration) {}
|
||||
// NOP by default.
|
||||
// TODO(#2496): Support people passing a func into the field.
|
||||
|
||||
/**
|
||||
* Bind handlers for user input on the text input field's editor.
|
||||
*
|
||||
* @param htmlInput The htmlInput to which event handlers will be bound.
|
||||
*/
|
||||
protected bindInputEvents_(htmlInput: HTMLElement) {
|
||||
// Trap Enter without IME and Esc to hide.
|
||||
this.onKeyDownWrapper_ = browserEvents.conditionalBind(
|
||||
htmlInput, 'keydown', this, this.onHtmlInputKeyDown_);
|
||||
// Resize after every input change.
|
||||
this.onKeyInputWrapper_ = browserEvents.conditionalBind(
|
||||
htmlInput, 'input', this, this.onHtmlInputChange_);
|
||||
}
|
||||
|
||||
/** Unbind handlers for user input and workspace size changes. */
|
||||
protected unbindInputEvents_() {
|
||||
if (this.onKeyDownWrapper_) {
|
||||
browserEvents.unbind(this.onKeyDownWrapper_);
|
||||
this.onKeyDownWrapper_ = null;
|
||||
}
|
||||
if (this.onKeyInputWrapper_) {
|
||||
browserEvents.unbind(this.onKeyInputWrapper_);
|
||||
this.onKeyInputWrapper_ = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key down to the editor.
|
||||
*
|
||||
* @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) {
|
||||
WidgetDiv.hide();
|
||||
dropDownDiv.hideWithoutAnimation();
|
||||
// AnyDuringMigration because: Property 'keyCode' does not exist on type
|
||||
// 'Event'.
|
||||
} else if ((e as AnyDuringMigration).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) {
|
||||
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);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a change to the editor.
|
||||
*
|
||||
* @param _e Keyboard event.
|
||||
*/
|
||||
private onHtmlInputChange_(_e: Event) {
|
||||
const text = this.htmlInput_!.value;
|
||||
if (text !== this.htmlInput_!.getAttribute('data-old-value')) {
|
||||
this.htmlInput_!.setAttribute('data-old-value', text);
|
||||
|
||||
const value = this.getValueFromEditorText_(text);
|
||||
this.setValue(value);
|
||||
this.forceRerender();
|
||||
this.resizeEditor_();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTML input value and the field's internal value. The difference
|
||||
* between this and `setValue` is that this also updates the HTML input
|
||||
* value whilst editing.
|
||||
*
|
||||
* @param newValue New value.
|
||||
*/
|
||||
protected setEditorValue_(newValue: AnyDuringMigration) {
|
||||
this.isDirty_ = true;
|
||||
if (this.isBeingEdited_) {
|
||||
// In the case this method is passed an invalid value, we still
|
||||
// pass it through the transformation method `getEditorText` to deal
|
||||
// with. Otherwise, the internal field's state will be inconsistent
|
||||
// with what's shown to the user.
|
||||
this.htmlInput_!.value = this.getEditorText_(newValue);
|
||||
}
|
||||
this.setValue(newValue);
|
||||
}
|
||||
|
||||
/** Resize the editor to fit the text. */
|
||||
protected resizeEditor_() {
|
||||
const block = this.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new UnattachedFieldError();
|
||||
}
|
||||
const div = WidgetDiv.getDiv();
|
||||
const bBox = this.getScaledBBox();
|
||||
div!.style.width = bBox.right - bBox.left + 'px';
|
||||
div!.style.height = bBox.bottom - bBox.top + 'px';
|
||||
|
||||
// In RTL mode block fields and LTR input fields the left edge moves,
|
||||
// whereas the right edge is fixed. Reposition the editor.
|
||||
const x = block.RTL ? bBox.right - div!.offsetWidth : bBox.left;
|
||||
const xy = new Coordinate(x, bBox.top);
|
||||
|
||||
div!.style.left = xy.x + 'px';
|
||||
div!.style.top = xy.y + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the field is tab navigable.
|
||||
*
|
||||
* @returns True if the field is tab navigable.
|
||||
*/
|
||||
override isTabNavigable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the `getText_` developer hook to override the field's text
|
||||
* representation. When we're currently editing, return the current HTML value
|
||||
* instead. Otherwise, return null which tells the field to use the default
|
||||
* behaviour (which is a string cast of the field's value).
|
||||
*
|
||||
* @returns The HTML value if we're editing, otherwise null.
|
||||
*/
|
||||
protected override getText_(): string|null {
|
||||
if (this.isBeingEdited_ && this.htmlInput_) {
|
||||
// We are currently editing, return the HTML input value instead.
|
||||
return this.htmlInput_.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the provided value into a text to show in the HTML input.
|
||||
* Override this method if the field's HTML input representation is different
|
||||
* than the field's value. This should be coupled with an override of
|
||||
* `getValueFromEditorText_`.
|
||||
*
|
||||
* @param value The value stored in this field.
|
||||
* @returns The text to show on the HTML input.
|
||||
*/
|
||||
protected getEditorText_(value: AnyDuringMigration): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the text received from the HTML input into a value to store
|
||||
* in this field.
|
||||
* Override this method if the field's HTML input representation is different
|
||||
* than the field's value. This should be coupled with an override of
|
||||
* `getEditorText_`.
|
||||
*
|
||||
* @param text Text received from the HTML input.
|
||||
* @returns The value to store.
|
||||
*/
|
||||
protected getValueFromEditorText_(text: string): AnyDuringMigration {
|
||||
return text;
|
||||
opt_value?: string|Sentinel, opt_validator?: FieldTextInputValidator|null,
|
||||
opt_config?: FieldInputConfig) {
|
||||
super(opt_value, opt_validator, opt_config);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -612,16 +64,11 @@ fieldRegistry.register('field_input', FieldTextInput);
|
||||
|
||||
(FieldTextInput.prototype as AnyDuringMigration).DEFAULT_VALUE = '';
|
||||
|
||||
/**
|
||||
* Config options for the text input field.
|
||||
*/
|
||||
export interface FieldTextInputConfig extends FieldConfig {
|
||||
spellcheck?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* fromJson config options for the text input field.
|
||||
*/
|
||||
export interface FieldTextInputFromJsonConfig extends FieldTextInputConfig {
|
||||
export interface FieldTextInputFromJsonConfig extends FieldInputConfig {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export {FieldInputConfig as FieldTextInputConfig};
|
||||
|
||||
@@ -17,7 +17,7 @@ import './events/events_block_change.js';
|
||||
|
||||
import type {Block} from './block.js';
|
||||
import {Field, FieldConfig, UnattachedFieldError} from './field.js';
|
||||
import {FieldDropdown, MenuGenerator, MenuOption} from './field_dropdown.js';
|
||||
import {FieldDropdown, FieldDropdownValidator, MenuGenerator, MenuOption} from './field_dropdown.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import * as internalConstants from './internal_constants.js';
|
||||
import type {Menu} from './menu.js';
|
||||
@@ -30,6 +30,7 @@ import {VariableModel} from './variable_model.js';
|
||||
import * as Variables from './variables.js';
|
||||
import * as Xml from './xml.js';
|
||||
|
||||
export type FieldVariableValidator = FieldDropdownValidator;
|
||||
|
||||
/**
|
||||
* Class for a variable's dropdown field.
|
||||
@@ -79,7 +80,7 @@ export class FieldVariable extends FieldDropdown {
|
||||
* for a list of properties this parameter supports.
|
||||
*/
|
||||
constructor(
|
||||
varName: string|null|Sentinel, opt_validator?: Function,
|
||||
varName: string|null|Sentinel, opt_validator?: FieldVariableValidator,
|
||||
opt_variableTypes?: string[], opt_defaultType?: string,
|
||||
opt_config?: FieldVariableConfig) {
|
||||
super(Field.SKIP_SETUP);
|
||||
|
||||
@@ -75,7 +75,7 @@ export class Input {
|
||||
* field again. Should be unique to the host block.
|
||||
* @returns The input being append to (to allow chaining).
|
||||
*/
|
||||
appendField(field: string|Field, opt_name?: string): Input {
|
||||
appendField<T>(field: string|Field<T>, opt_name?: string): Input {
|
||||
this.insertFieldAt(this.fieldRow.length, field, opt_name);
|
||||
return this;
|
||||
}
|
||||
@@ -90,7 +90,8 @@ export class Input {
|
||||
* field again. Should be unique to the host block.
|
||||
* @returns The index following the last inserted field.
|
||||
*/
|
||||
insertFieldAt(index: number, field: string|Field, opt_name?: string): number {
|
||||
insertFieldAt<T>(index: number, field: string|Field<T>, opt_name?: string):
|
||||
number {
|
||||
if (index < 0 || index > this.fieldRow.length) {
|
||||
throw Error('index ' + index + ' out of bounds.');
|
||||
}
|
||||
@@ -103,9 +104,9 @@ export class Input {
|
||||
// Generate a FieldLabel when given a plain text field.
|
||||
if (typeof field === 'string') {
|
||||
field = fieldRegistry.fromJson({
|
||||
'type': 'field_label',
|
||||
'text': field,
|
||||
}) as Field;
|
||||
type: 'field_label',
|
||||
text: field,
|
||||
})!;
|
||||
}
|
||||
|
||||
field.setSourceBlock(this.sourceBlock_);
|
||||
@@ -121,7 +122,7 @@ export class Input {
|
||||
index = this.insertFieldAt(index, field.prefixField);
|
||||
}
|
||||
// Add the field to the field row.
|
||||
this.fieldRow.splice(index, 0, field);
|
||||
this.fieldRow.splice(index, 0, field as Field);
|
||||
index++;
|
||||
if (field.suffixField) {
|
||||
// Add any suffix.
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* The interface for a Blockly field that can be registered.
|
||||
*
|
||||
* @namespace Blockly.IRegistrableField
|
||||
*/
|
||||
import * as goog from '../../closure/goog/goog.js';
|
||||
goog.declareModuleId('Blockly.IRegistrableField');
|
||||
|
||||
import type {Field} from '../field.js';
|
||||
|
||||
|
||||
type fromJson = (p1: object) => Field;
|
||||
|
||||
/**
|
||||
* A registrable field.
|
||||
* Note: We are not using an interface here as we are interested in defining the
|
||||
* static methods of a field rather than the instance methods.
|
||||
*
|
||||
* @alias Blockly.IRegistrableField
|
||||
*/
|
||||
export interface IRegistrableField {
|
||||
fromJson: fromJson;
|
||||
}
|
||||
Reference in New Issue
Block a user