namespace gdjs { const supportedInputTypes = [ 'text', 'email', 'password', 'number', 'telephone number', 'url', 'search', 'text area', ] as const; const supportedTextAlign = ['left', 'center', 'right'] as const; type SupportedInputType = (typeof supportedInputTypes)[number]; type SupportedTextAlign = (typeof supportedTextAlign)[number]; const parseInputType = (potentialInputType: string): SupportedInputType => { const lowercasedNewInputType = potentialInputType.toLowerCase(); // @ts-ignore - we're actually checking that this value is correct. if (supportedInputTypes.includes(lowercasedNewInputType)) return potentialInputType as SupportedInputType; return 'text'; }; const parseTextAlign = ( potentialTextAlign: string | undefined ): SupportedTextAlign => { if (!potentialTextAlign) return 'left'; const lowercasedNewTextAlign = potentialTextAlign.toLowerCase(); // @ts-ignore - we're actually checking that this value is correct. if (supportedTextAlign.includes(lowercasedNewTextAlign)) return potentialTextAlign as SupportedTextAlign; return 'left'; }; /** * Base parameters for {@link gdjs.TextInputRuntimeObject} * @category Objects > Text Input */ export interface TextInputObjectData extends ObjectData { /** The base parameters of the TextInput */ content: { initialValue: string; placeholder: string; fontResourceName: string; fontSize: float; inputType: SupportedInputType; textColor: string; fillColor: string; fillOpacity: float; borderColor: string; borderOpacity: float; borderWidth: float; disabled: boolean; readOnly: boolean; // ---- Values can be undefined because of support for these feature was added in v5.5.222. spellCheck?: boolean; paddingX?: float; paddingY?: float; textAlign?: SupportedTextAlign; maxLength?: integer; // ---- }; } /** * @category Objects > Text Input */ export type TextInputNetworkSyncDataType = { opa: float; txt: string; frn: string; fs: number; place: string; it: SupportedInputType; tc: string; fc: string; fo: float; bc: string; bo: float; bw: float; dis: boolean; ro: boolean; sc: boolean; }; /** * @category Objects > Text Input */ export type TextInputNetworkSyncData = ObjectNetworkSyncData & TextInputNetworkSyncDataType; const DEFAULT_WIDTH = 300; const DEFAULT_HEIGHT = 30; const clampPadding = (value: float, dimension: float, borderWidth: float) => { return Math.max(0, Math.min(dimension / 2 - borderWidth, value)); }; /** * Shows a text input on the screen the player can type text into. * @category Objects > Text Input */ export class TextInputRuntimeObject extends gdjs.RuntimeObject implements gdjs.TextContainer, gdjs.Resizable, gdjs.OpacityHandler { private _string: string; private _placeholder: string; private opacity: float = 255; private _width: float = DEFAULT_WIDTH; private _height: float = DEFAULT_HEIGHT; private _fontResourceName: string; private _fontSize: float; private _inputType: SupportedInputType; private _textColor: [float, float, float]; private _fillColor: [float, float, float]; private _fillOpacity: float; private _paddingX: integer; private _paddingY: integer; private _textAlign: SupportedTextAlign; private _maxLength: integer; private _borderColor: [float, float, float]; private _borderOpacity: float; private _borderWidth: float; private _disabled: boolean; private _readOnly: boolean; private _spellCheck: boolean; private _isSubmitted: boolean; _renderer: TextInputRuntimeObjectRenderer; constructor( instanceContainer: gdjs.RuntimeInstanceContainer, objectData: TextInputObjectData, instanceData?: InstanceData ) { super(instanceContainer, objectData, instanceData); this._string = objectData.content.initialValue; this._placeholder = objectData.content.placeholder; this._fontResourceName = objectData.content.fontResourceName; this._fontSize = objectData.content.fontSize || 20; this._inputType = parseInputType(objectData.content.inputType); this._textColor = gdjs.rgbOrHexToRGBColor(objectData.content.textColor); this._fillColor = gdjs.rgbOrHexToRGBColor(objectData.content.fillColor); this._fillOpacity = objectData.content.fillOpacity; this._borderColor = gdjs.rgbOrHexToRGBColor( objectData.content.borderColor ); this._borderOpacity = objectData.content.borderOpacity; this._borderWidth = objectData.content.borderWidth; this._disabled = instanceContainer.getGame().isInGameEdition() ? true : objectData.content.disabled; this._readOnly = objectData.content.readOnly; this._spellCheck = objectData.content.spellCheck !== undefined ? objectData.content.spellCheck : false; this._textAlign = parseTextAlign(objectData.content.textAlign); this._maxLength = objectData.content.maxLength || 0; this._paddingX = objectData.content.paddingX !== undefined ? objectData.content.paddingX : 2; this._paddingY = objectData.content.paddingY !== undefined ? objectData.content.paddingY : 1; this._isSubmitted = false; this._renderer = new gdjs.TextInputRuntimeObjectRenderer( this, instanceContainer ); // *ALWAYS* call `this.onCreated()` at the very end of your object constructor. this.onCreated(); } getRendererObject() { return null; } updateFromObjectData( oldObjectData: TextInputObjectData, newObjectData: TextInputObjectData ): boolean { if ( oldObjectData.content.initialValue !== newObjectData.content.initialValue ) { if (this._string === oldObjectData.content.initialValue) { this.setString(newObjectData.content.initialValue); } } if ( oldObjectData.content.placeholder !== newObjectData.content.placeholder ) { this.setPlaceholder(newObjectData.content.placeholder); } if ( oldObjectData.content.fontResourceName !== newObjectData.content.fontResourceName ) { this.setFontResourceName(newObjectData.content.fontResourceName); } if (oldObjectData.content.fontSize !== newObjectData.content.fontSize) { this.setFontSize(newObjectData.content.fontSize); } if (oldObjectData.content.inputType !== newObjectData.content.inputType) { this.setInputType(newObjectData.content.inputType); } if (oldObjectData.content.textColor !== newObjectData.content.textColor) { this.setTextColor(newObjectData.content.textColor); } if (oldObjectData.content.fillColor !== newObjectData.content.fillColor) { this.setFillColor(newObjectData.content.fillColor); } if ( oldObjectData.content.fillOpacity !== newObjectData.content.fillOpacity ) { this.setFillOpacity(newObjectData.content.fillOpacity); } if ( oldObjectData.content.borderColor !== newObjectData.content.borderColor ) { this.setBorderColor(newObjectData.content.borderColor); } if ( oldObjectData.content.borderOpacity !== newObjectData.content.borderOpacity ) { this.setBorderOpacity(newObjectData.content.borderOpacity); } if ( oldObjectData.content.borderWidth !== newObjectData.content.borderWidth ) { this.setBorderWidth(newObjectData.content.borderWidth); } if (oldObjectData.content.disabled !== newObjectData.content.disabled) { this.setDisabled(newObjectData.content.disabled); } if (oldObjectData.content.readOnly !== newObjectData.content.readOnly) { this.setReadOnly(newObjectData.content.readOnly); } if ( newObjectData.content.spellCheck !== undefined && oldObjectData.content.spellCheck !== newObjectData.content.spellCheck ) { this.setSpellCheck(newObjectData.content.spellCheck); } if ( newObjectData.content.maxLength !== undefined && oldObjectData.content.maxLength !== newObjectData.content.maxLength ) { this.setMaxLength(newObjectData.content.maxLength); } if ( newObjectData.content.textAlign && oldObjectData.content.textAlign !== newObjectData.content.textAlign ) { this._textAlign = newObjectData.content.textAlign; } if ( newObjectData.content.paddingX !== undefined && oldObjectData.content.paddingX !== newObjectData.content.paddingX ) { this.setPaddingX(newObjectData.content.paddingX); } if ( newObjectData.content.paddingY !== undefined && oldObjectData.content.paddingY !== newObjectData.content.paddingY ) { this.setPaddingY(newObjectData.content.paddingY); } return true; } getNetworkSyncData( syncOptions: GetNetworkSyncDataOptions ): TextInputNetworkSyncData { return { ...super.getNetworkSyncData(syncOptions), opa: this.getOpacity(), txt: this.getText(), frn: this.getFontResourceName(), fs: this.getFontSize(), place: this.getPlaceholder(), it: this.getInputType(), tc: this.getTextColor(), fc: this.getFillColor(), fo: this.getFillOpacity(), bc: this.getBorderColor(), bo: this.getBorderOpacity(), bw: this.getBorderWidth(), dis: this.isDisabled(), ro: this.isReadOnly(), sc: this.isSpellCheckEnabled(), }; } updateFromNetworkSyncData( syncData: TextInputNetworkSyncData, options: UpdateFromNetworkSyncDataOptions ): void { super.updateFromNetworkSyncData(syncData, options); if (syncData.opa !== undefined) this.setOpacity(syncData.opa); if (syncData.txt !== undefined) this.setText(syncData.txt); if (syncData.frn !== undefined) this.setFontResourceName(syncData.frn); if (syncData.fs !== undefined) this.setFontSize(syncData.fs); if (syncData.place !== undefined) this.setPlaceholder(syncData.place); if (syncData.it !== undefined) this.setInputType(syncData.it); if (syncData.tc !== undefined) this.setTextColor(syncData.tc); if (syncData.fc !== undefined) this.setFillColor(syncData.fc); if (syncData.fo !== undefined) this.setFillOpacity(syncData.fo); if (syncData.bc !== undefined) this.setBorderColor(syncData.bc); if (syncData.bo !== undefined) this.setBorderOpacity(syncData.bo); if (syncData.bw !== undefined) this.setBorderWidth(syncData.bw); if (syncData.dis !== undefined) this.setDisabled(syncData.dis); if (syncData.ro !== undefined) this.setReadOnly(syncData.ro); if (syncData.sc !== undefined) this.setSpellCheck(syncData.sc); } updatePreRender(instanceContainer: RuntimeInstanceContainer): void { this._isSubmitted = false; this._renderer.updatePreRender(); } /** * Initialize the extra parameters that could be set for an instance. */ extraInitializationFromInitialInstance(initialInstanceData: InstanceData) { for (const property of initialInstanceData.stringProperties) { if (property.name === 'initialValue') { this.setString(property.value); } else if (property.name === 'placeholder') { this.setPlaceholder(property.value); } } if (initialInstanceData.customSize) { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); this._renderer.updatePadding(); } this.setOpacity( initialInstanceData.opacity === undefined ? 255 : initialInstanceData.opacity ); } onScenePaused(runtimeScene: gdjs.RuntimeScene): void { this._renderer.onScenePaused(); } onSceneResumed(runtimeScene: gdjs.RuntimeScene): void { this._renderer.onSceneResumed(); } onDestroyed(): void { super.onDestroyed(); this._renderer.onDestroy(); } setOpacity(opacity: float): void { this.opacity = Math.max(0, Math.min(255, opacity)); this._renderer.updateOpacity(); } getOpacity(): float { return this.opacity; } setSize(width: number, height: number): void { this.setWidth(width); this.setHeight(height); } setWidth(width: float): void { this._width = width; this._renderer.updatePadding(); } setHeight(height: float): void { this._height = height; this._renderer.updatePadding(); } /** * Return the width of the object. * @return The width of the object */ getWidth(): float { return this._width; } /** * Return the width of the object. * @return The height of the object */ getHeight(): float { return this._height; } /** * Get the text entered in the text input. * @deprecated use `getText` instead */ getString() { return this.getText(); } /** * Replace the text inside the text input. * @deprecated use `setText` instead */ setString(text: string) { this.setText(text); } getText() { return this._string; } setText(newString: string) { if (newString === this._string) return; this._string = newString; this._renderer.updateString(); } /** * Called by the renderer when the value of the input shown on the screen * was changed (because the user typed something). * This does not propagate back the value to the renderer, which would * result in the cursor being sent back to the end of the text. * * Do not use this if you are not inside the renderer - use `setString` instead. */ onRendererInputValueChanged(inputValue: string) { this._string = inputValue; } onRendererFormSubmitted() { this._isSubmitted = true; } getFontResourceName() { return this._fontResourceName; } setFontResourceName(resourceName: string) { if (this._fontResourceName === resourceName) return; this._fontResourceName = resourceName; this._renderer.updateFont(); } getFontSize() { return this._fontSize; } setFontSize(newSize: number) { this._fontSize = newSize; } /** * Get the placeholder shown when no text is entered */ getPlaceholder() { return this._placeholder; } /** * Replace the text inside the text input. */ setPlaceholder(newPlaceholder: string) { if (newPlaceholder === this._placeholder) return; this._placeholder = newPlaceholder; this._renderer.updatePlaceholder(); } /** * Get the type of the input. */ getInputType() { return this._inputType; } /** * Set the type of the input. */ setInputType(newInputType: string) { const lowercasedNewInputType = newInputType.toLowerCase(); if (lowercasedNewInputType === this._inputType) return; this._inputType = parseInputType(lowercasedNewInputType); this._renderer.updateInputType(); } setTextColor(newColor: string) { this._textColor = gdjs.rgbOrHexToRGBColor(newColor); this._renderer.updateTextColor(); } getTextColor(): string { return ( this._textColor[0] + ';' + this._textColor[1] + ';' + this._textColor[2] ); } _getRawTextColor(): [float, float, float] { return this._textColor; } setFillColor(newColor: string) { this._fillColor = gdjs.rgbOrHexToRGBColor(newColor); this._renderer.updateFillColorAndOpacity(); } getFillColor(): string { return ( this._fillColor[0] + ';' + this._fillColor[1] + ';' + this._fillColor[2] ); } _getRawFillColor(): [float, float, float] { return this._fillColor; } setFillOpacity(newOpacity: float) { this._fillOpacity = Math.max(0, Math.min(255, newOpacity)); this._renderer.updateFillColorAndOpacity(); } getFillOpacity(): float { return this._fillOpacity; } setBorderColor(newColor: string) { this._borderColor = gdjs.rgbOrHexToRGBColor(newColor); this._renderer.updateBorderColorAndOpacity(); } getBorderColor(): string { return ( this._borderColor[0] + ';' + this._borderColor[1] + ';' + this._borderColor[2] ); } _getRawBorderColor(): [float, float, float] { return this._borderColor; } setBorderOpacity(newOpacity: float) { this._borderOpacity = Math.max(0, Math.min(255, newOpacity)); this._renderer.updateBorderColorAndOpacity(); } getBorderOpacity(): float { return this._borderOpacity; } setBorderWidth(width: float) { this._borderWidth = Math.max(0, width); this._renderer.updateBorderWidth(); } getBorderWidth(): float { return this._borderWidth; } setDisabled(value: boolean) { if (this.getInstanceContainer().getGame().isInGameEdition()) { return; } this._disabled = value; this._renderer.updateDisabled(); } isDisabled(): boolean { return this._disabled; } setReadOnly(value: boolean) { this._readOnly = value; this._renderer.updateReadOnly(); } isReadOnly(): boolean { return this._readOnly; } setSpellCheck(value: boolean) { this._spellCheck = value; this._renderer.updateSpellCheck(); } isSpellCheckEnabled(): boolean { return this._spellCheck; } isFocused(): boolean { return this._renderer.isFocused(); } isSubmitted(): boolean { return this._isSubmitted; } getMaxLength(): integer { return this._maxLength; } setMaxLength(value: integer) { if (this._maxLength === value) return; this._maxLength = value; this._renderer.updateMaxLength(); } getPaddingX(): integer { return clampPadding(this._paddingX, this._width, this._borderWidth); } setPaddingX(value: integer) { if (this._paddingX === value) return; if (value < 0) { this._paddingX = 0; return; } this._paddingX = value; this._renderer.updatePadding(); } getPaddingY(): integer { return clampPadding(this._paddingY, this._height, this._borderWidth); } setPaddingY(value: integer) { if (this._paddingY === value) return; if (value < 0) { this._paddingY = 0; return; } this._paddingY = value; this._renderer.updatePadding(); } getTextAlign(): SupportedTextAlign { return this._textAlign; } setTextAlign(newTextAlign: string) { const parsedTextAlign = parseTextAlign(newTextAlign); if (parsedTextAlign === this._textAlign) return; this._textAlign = parsedTextAlign; this._renderer.updateTextAlign(); } focus(): void { if (!this.isFocused()) { // If the input was not previously focused, reset input manager because there is // no reason to maintain its state. It avoids bugs where a key is pressed, the text // input is focused and then the input manager does not have access to the keyup event // and considers the key still pressed. this.getInstanceContainer() .getGame() .getInputManager() .clearAllPressedKeys(); } this._renderer.focus(); } } gdjs.registerObject( 'TextInput::TextInputObject', gdjs.TextInputRuntimeObject ); }