//@ts-check /// /** * This is a declaration of an extension for GDevelop 5. * * ℹ️ Changes in this file are watched and automatically imported if the editor * is running. You can also manually run `node import-GDJS-Runtime.js` (in newIDE/app/scripts). * * The file must be named "JsExtension.js", otherwise GDevelop won't load it. * ⚠️ If you make a change and the extension is not loaded, open the developer console * and search for any errors. * * More information on https://github.com/4ian/GDevelop/blob/master/newIDE/README-extensions.md */ /** @type {ExtensionModule} */ module.exports = { createExtension: function (_, gd) { const extension = new gd.PlatformExtension(); extension .setExtensionInformation( 'TextInput', _('Text Input'), _('A text field the player can type text into.'), 'Florian Rival', 'MIT' ) .setShortDescription( 'Text input field for players. Placeholder, font, color, disabled state, read-only, focus control.' ) .setCategory('User interface'); extension .addInstructionOrExpressionGroupMetadata(_('Text Input')) .setIcon('JsPlatform/Extensions/text_input.svg'); const textInputObject = new gd.ObjectJsImplementation(); textInputObject.updateProperty = function (propertyName, newValue) { const objectContent = this.content; if (propertyName === 'initialValue') { objectContent.initialValue = newValue; return true; } else if (propertyName === 'placeholder') { objectContent.placeholder = newValue; return true; } else if (propertyName === 'fontResourceName') { objectContent.fontResourceName = newValue; return true; } else if (propertyName === 'fontSize') { objectContent.fontSize = Math.max(1, parseFloat(newValue)); return true; } else if (propertyName === 'inputType') { const normalizedValue = newValue.toLowerCase(); if ( normalizedValue === 'text' || normalizedValue === 'text area' || normalizedValue === 'email' || normalizedValue === 'password' || normalizedValue === 'number' || normalizedValue === 'telephone number' || normalizedValue === 'url' || normalizedValue === 'search' ) { objectContent.inputType = normalizedValue; return true; } return false; } else if (propertyName === 'textColor') { objectContent.textColor = newValue; return true; } else if (propertyName === 'fillColor') { objectContent.fillColor = newValue; return true; } else if (propertyName === 'fillOpacity') { objectContent.fillOpacity = Math.max( 0, Math.min(255, parseFloat(newValue)) ); return true; } else if (propertyName === 'borderColor') { objectContent.borderColor = newValue; return true; } else if (propertyName === 'borderOpacity') { objectContent.borderOpacity = Math.max( 0, Math.min(255, parseFloat(newValue)) ); return true; } else if (propertyName === 'borderWidth') { objectContent.borderWidth = Math.max(0, parseFloat(newValue)); return true; } else if (propertyName === 'readOnly') { objectContent.readOnly = newValue === '1'; return true; } else if (propertyName === 'disabled') { objectContent.disabled = newValue === '1'; return true; } else if (propertyName === 'spellCheck') { objectContent.spellCheck = newValue === '1'; return true; } else if (propertyName === 'maxLength') { objectContent.maxLength = newValue; return true; } else if (propertyName === 'paddingX') { objectContent.paddingX = Math.max(0, parseFloat(newValue)); return true; } else if (propertyName === 'paddingY') { objectContent.paddingY = Math.max(0, parseFloat(newValue)); return true; } else if (propertyName === 'textAlign') { const normalizedValue = newValue.toLowerCase(); if ( normalizedValue === 'left' || normalizedValue === 'center' || normalizedValue === 'right' ) { objectContent.textAlign = normalizedValue; return true; } return false; } return false; }; textInputObject.getProperties = function () { const objectProperties = new gd.MapStringPropertyDescriptor(); const objectContent = this.content; objectProperties .getOrCreate('initialValue') .setValue(objectContent.initialValue) .setType('string') .setLabel(_('Initial value')) .setGroup(_('Content')); objectProperties .getOrCreate('placeholder') .setValue(objectContent.placeholder) .setType('string') .setLabel(_('Placeholder')) .setGroup(_('Content')); objectProperties .getOrCreate('fontResourceName') .setValue(objectContent.fontResourceName || '') .setType('resource') .addExtraInfo('font') .setLabel(_('Font')) .setGroup(_('Font')); objectProperties .getOrCreate('fontSize') .setValue((objectContent.fontSize || 20).toString()) .setType('number') .setLabel(_('Font size (px)')) .setGroup(_('Font')); objectProperties .getOrCreate('inputType') .setValue(objectContent.inputType || '') .setType('choice') .addChoice('text', _('Text')) .addChoice('text area', _('Text area')) .addChoice('email', _('Email')) .addChoice('password', _('Password')) .addChoice('number', _('Number')) .addChoice('telephone number', _('Telephone number')) .addChoice('url', _('URL')) .addChoice('search', _('Search')) .setLabel(_('Input type')) .setDescription( _( 'By default, a "text" is single line. Choose "text area" to allow multiple lines to be entered.' ) ); objectProperties .getOrCreate('readOnly') .setValue(objectContent.readOnly ? 'true' : 'false') .setType('boolean') .setLabel(_('Read only')) .setGroup(_('Field')); objectProperties .getOrCreate('disabled') .setValue(objectContent.disabled ? 'true' : 'false') .setType('boolean') .setLabel(_('Disabled')) .setGroup(_('Field')); objectProperties .getOrCreate('spellCheck') .setValue(objectContent.spellCheck ? 'true' : 'false') .setType('boolean') .setLabel(_('Enable spell check')) .setGroup(_('Field')); objectProperties .getOrCreate('textColor') .setValue(objectContent.textColor || '0;0;0') .setType('color') .setLabel(_('Text color')) .setGroup(_('Font')); objectProperties .getOrCreate('fillColor') .setValue(objectContent.fillColor || '255;255;255') .setType('color') .setLabel(_('Fill color')) .setGroup(_('Field appearance')); objectProperties .getOrCreate('fillOpacity') .setValue( (objectContent.fillOpacity != undefined ? objectContent.fillOpacity : 255 ).toString() ) .setType('number') .setLabel(_('Fill opacity')) .setGroup(_('Field appearance')); objectProperties .getOrCreate('borderColor') .setValue(objectContent.borderColor || '0;0;0') .setType('color') .setLabel(_('Color')) .setGroup(_('Border appearance')); objectProperties .getOrCreate('borderOpacity') .setValue( (objectContent.borderOpacity != undefined ? objectContent.borderOpacity : 255 ).toString() ) .setType('number') .setLabel(_('Opacity')) .setGroup(_('Border appearance')); objectProperties .getOrCreate('borderWidth') .setValue((objectContent.borderWidth || 0).toString()) .setType('number') .setLabel(_('Width')) .setGroup(_('Border appearance')); objectProperties .getOrCreate('paddingX') .setValue( (objectContent.paddingX !== undefined ? objectContent.paddingX : 2 ).toString() ) .setType('number') .setLabel(_('Padding (horizontal)')) .setGroup(_('Field appearance')); objectProperties .getOrCreate('paddingY') .setValue( (objectContent.paddingY !== undefined ? objectContent.paddingY : 1 ).toString() ) .setType('number') .setLabel(_('Padding (vertical)')) .setGroup(_('Field appearance')); objectProperties .getOrCreate('maxLength') .setValue((objectContent.maxLength || 0).toString()) .setType('number') .setLabel(_('Max length')) .setDescription( _( 'The maximum length of the input value (this property will be ignored if the input type is a number).' ) ) .setAdvanced(true); objectProperties .getOrCreate('textAlign') .setValue(objectContent.textAlign || 'left') .setType('choice') .addChoice('left', _('Left')) .addChoice('center', _('Center')) .addChoice('right', _('Right')) .setLabel(_('Text alignment')) .setGroup(_('Field appearance')); return objectProperties; }; textInputObject.content = { initialValue: '', placeholder: 'Touch to start typing', fontResourceName: '', fontSize: 20, inputType: 'text', textColor: '0;0;0', fillColor: '255;255;255', fillOpacity: 255, borderColor: '0;0;0', borderOpacity: 255, borderWidth: 1, readOnly: false, disabled: false, spellCheck: false, paddingX: 2, paddingY: 1, textAlign: 'left', maxLength: 0, }; textInputObject.updateInitialInstanceProperty = function ( instance, propertyName, newValue ) { if (propertyName === 'initialValue') { instance.setRawStringProperty('initialValue', newValue); return true; } else if (propertyName === 'placeholder') { instance.setRawStringProperty('placeholder', newValue); return true; } return false; }; textInputObject.getInitialInstanceProperties = function (instance) { const instanceProperties = new gd.MapStringPropertyDescriptor(); instanceProperties .getOrCreate('initialValue') .setValue(instance.getRawStringProperty('initialValue')) .setType('string') .setLabel(_('Initial value')); instanceProperties .getOrCreate('placeholder') .setValue(instance.getRawStringProperty('placeholder')) .setType('string') .setLabel(_('Placeholder')); return instanceProperties; }; const object = extension .addObject( 'TextInputObject', _('Text input'), _('A text field the player can type text into.'), 'JsPlatform/Extensions/text_input.svg', textInputObject ) .setCategory('User interface') // Effects are unsupported because the object is not rendered with PIXI. .setIncludeFile('Extensions/TextInput/textinputruntimeobject.js') .addIncludeFile( 'Extensions/TextInput/textinputruntimeobject-pixi-renderer.js' ) .addDefaultBehavior('TextContainerCapability::TextContainerBehavior') .addDefaultBehavior('ResizableCapability::ResizableBehavior') .addDefaultBehavior('OpacityCapability::OpacityBehavior'); // Properties expressions/conditions/actions: // Deprecated, see TextContainerCapability object .addExpressionAndConditionAndAction( 'string', 'Text', _('Text'), _('the text'), _('the text'), '', 'res/conditions/text24_black.png' ) .setHidden() .addParameter('object', _('Text input'), 'TextInputObject', false) .useStandardParameters( 'string', gd.ParameterOptions.makeNewOptions().setDescription(_('Text')) ) .setFunctionName('setText') .setGetter('getText'); // Deprecated, see TextContainerCapability object .addStrExpression( 'Text', _('Text'), _('Return the text.'), '', 'res/conditions/text24_black.png' ) .setHidden() .addParameter('object', _('Text input'), 'TextInputObject', false) .setFunctionName('getText'); object .addExpressionAndConditionAndAction( 'string', 'Placeholder', _('Placeholder'), _('the placeholder'), _('the placeholder'), '', 'res/conditions/text24_black.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .useStandardParameters( 'string', gd.ParameterOptions.makeNewOptions().setDescription(_('Text')) ) .setFunctionName('setPlaceholder') .setGetter('getPlaceholder'); object .addExpressionAndConditionAndAction( 'number', 'Font size', _('Font size'), _('the font size'), _('the font size'), _('Font'), 'res/conditions/opacity24.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) .setFunctionName('setFontSize') .setGetter('getFontSize'); object .addExpressionAndCondition( 'string', 'FontResourceName', _('Font name'), _('the font name'), _('the font name'), _('Font'), 'res/conditions/font24.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .useStandardParameters('string', gd.ParameterOptions.makeNewOptions()) .setFunctionName('getFontResourceName'); // TODO: could this be merged with the previous expression and condition? object .addScopedAction( 'SetFontResourceName', _('Font name'), _('Set the font of the object.'), _('Set the font of _PARAM0_ to _PARAM1_'), _('Font'), 'res/actions/font24.png', 'res/actions/font.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .addParameter('fontResource', _('Font resource name'), '', false) .getCodeExtraInformation() .setFunctionName('setFontResourceName'); object .addExpressionAndConditionAndAction( 'string', 'InputType', _('Input type'), _('the input type'), _('the input type'), _('Type'), 'res/conditions/text24_black.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .useStandardParameters( 'stringWithSelector', gd.ParameterOptions.makeNewOptions() .setDescription(_('Input type')) .setTypeExtraInfo( JSON.stringify([ 'text', 'text area', 'email', 'password', 'number', 'telephone number', 'url', 'search', ]) ) ) .setFunctionName('setInputType') .setGetter('getInputType'); object .addScopedAction( 'SetTextColor', _('Text color'), _('Set the text color of the object.'), _('Set the text color of _PARAM0_ to _PARAM1_'), _('Field appearance'), 'res/actions/color24.png', 'res/actions/color.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .addParameter('color', _('Color'), '', false) .getCodeExtraInformation() .setFunctionName('setTextColor'); object .addScopedAction( 'SetFillColor', _('Fill color'), _('Set the fill color of the object.'), _('Set the fill color of _PARAM0_ to _PARAM1_'), _('Field appearance'), 'res/actions/color24.png', 'res/actions/color.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .addParameter('color', _('Color'), '', false) .getCodeExtraInformation() .setFunctionName('setFillColor'); object .addExpressionAndConditionAndAction( 'number', 'FillOpacity', _('Fill opacity'), _('the fill opacity, between 0 (fully transparent) and 255 (opaque)'), _('the fill opacity'), _('Field appearance'), 'res/conditions/opacity24.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .useStandardParameters( 'number', gd.ParameterOptions.makeNewOptions().setDescription( _('Opacity (0-255)') ) ) .setFunctionName('setFillOpacity') .setGetter('getFillOpacity'); object .addScopedAction( 'SetBorderColor', _('Border color'), _('Set the border color of the object.'), _('Set the border color of _PARAM0_ to _PARAM1_'), _('Field appearance'), 'res/actions/color24.png', 'res/actions/color.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .addParameter('color', _('Color'), '', false) .getCodeExtraInformation() .setFunctionName('setBorderColor'); object .addExpressionAndConditionAndAction( 'number', 'BorderOpacity', _('Border opacity'), _('the border opacity, between 0 (fully transparent) and 255 (opaque)'), _('the border opacity'), _('Field appearance'), 'res/conditions/opacity24.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .useStandardParameters( 'number', gd.ParameterOptions.makeNewOptions().setDescription( _('Opacity (0-255)') ) ) .setFunctionName('setBorderOpacity') .setGetter('getBorderOpacity'); object .addExpressionAndConditionAndAction( 'number', 'BorderWidth', _('Border width'), _('the border width'), _('the border width'), _('Field appearance'), 'res/conditions/outlineSize24_black.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) .setFunctionName('setBorderWidth') .setGetter('getBorderWidth'); // TODO: expressions for colors? object .addExpressionAndConditionAndAction( 'boolean', 'ReadOnly', _('Read-only'), _('the text input is read-only'), _('read-only'), '', 'res/conditions/text24_black.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .useStandardParameters( 'boolean', gd.ParameterOptions.makeNewOptions().setDescription(_('Read-only?')) ) .setFunctionName('setReadOnly') .setGetter('isReadOnly'); object .addExpressionAndConditionAndAction( 'boolean', 'Disabled', _('Disabled'), _('the text input is disabled'), _('disabled'), '', 'res/conditions/text24_black.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .useStandardParameters('boolean', gd.ParameterOptions.makeNewOptions()) .setFunctionName('setDisabled') .setGetter('isDisabled'); object .addExpressionAndConditionAndAction( 'boolean', 'SpellCheck', _('Spell check enabled'), _('spell check is enabled'), _('spell check enabled'), '', 'res/conditions/text24_black.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .useStandardParameters('boolean', gd.ParameterOptions.makeNewOptions()) .setFunctionName('setSpellCheck') .setGetter('isSpellCheckEnabled'); // Other expressions/conditions/actions: // Deprecated object .addExpressionAndConditionAndAction( 'number', 'Opacity', _('Opacity'), _('the opacity, between 0 (fully transparent) and 255 (opaque)'), _('the opacity'), '', 'res/conditions/opacity24.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .useStandardParameters( 'number', gd.ParameterOptions.makeNewOptions().setDescription( _('Opacity (0-255)') ) ) .setFunctionName('setOpacity') .setGetter('getOpacity') .setHidden(); object .addScopedCondition( 'Focused', _('Focused'), _( 'Check if the text input is focused (the cursor is in the field and player can type text in).' ), _('_PARAM0_ is focused'), '', 'res/conditions/surObjet24.png', 'res/conditions/surObjet.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .getCodeExtraInformation() .setFunctionName('isFocused'); object .addScopedCondition( 'IsInputSubmitted', _('Input is submitted'), _( 'Check if the input is submitted, which usually happens when the Enter key is pressed on a keyboard, or a specific button on mobile virtual keyboards.' ), _('_PARAM0_ value was submitted'), '', 'res/conditions/surObjet24.png', 'res/conditions/surObjet.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .getCodeExtraInformation() .setFunctionName('isSubmitted'); object .addScopedAction( 'Focus', _('Focus'), _( 'Focus the input so that text can be entered (like if it was touched/clicked).' ), _('Focus _PARAM0_'), _(''), 'res/conditions/surObjet24.png', 'res/conditions/surObjet.png' ) .addParameter('object', _('Text input'), 'TextInputObject', false) .getCodeExtraInformation() .setFunctionName('focus'); return extension; }, /** * You can optionally add sanity tests that will check the basic working * of your extension behaviors/objects by instantiating behaviors/objects * and setting the property to a given value. * * If you don't have any tests, you can simply return an empty array. * * But it is recommended to create tests for the behaviors/objects properties you created * to avoid mistakes. */ runExtensionSanityTests: function (gd, extension) { return []; }, /** * Register editors for objects. * * ℹ️ Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change. */ registerEditorConfigurations: function (objectsEditorService) { objectsEditorService.registerEditorConfiguration( 'TextInput::TextInputObject', objectsEditorService.getDefaultObjectJsImplementationPropertiesEditor({ helpPagePath: '/objects/text_input', }) ); }, /** * Register renderers for instance of objects on the scene editor. * * ℹ️ Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change. */ registerInstanceRenderers: function (objectsRenderingService) { const RenderedInstance = objectsRenderingService.RenderedInstance; const PIXI = objectsRenderingService.PIXI; const DEFAULT_WIDTH = 300; const DEFAULT_HEIGHT = 30; class RenderedTextInputObjectInstance extends RenderedInstance { constructor( project, instance, associatedObjectConfiguration, pixiContainer, pixiResourcesLoader ) { super( project, instance, associatedObjectConfiguration, pixiContainer, pixiResourcesLoader ); this._fontResourceName = ''; this._finalTextColor = 0x0; this._pixiGraphics = new PIXI.Graphics(); this._pixiTextMask = new PIXI.Graphics(); this._pixiText = new PIXI.Text(' ', { align: 'left', fontSize: 20, }); this._pixiText.mask = this._pixiTextMask; this._pixiObject = new PIXI.Container(); this._pixiObject.addChild(this._pixiGraphics); this._pixiObject.addChild(this._pixiText); this._pixiObject.addChild(this._pixiTextMask); this._pixiContainer.addChild(this._pixiObject); this.update(); } onRemovedFromScene() { super.onRemovedFromScene(); this._pixiText.destroy(true); this._pixiObject.destroy({ children: true }); } static getThumbnail(project, resourcesLoader, objectConfiguration) { return 'JsPlatform/Extensions/text_input.svg'; } update() { const instance = this._instance; const object = gd.castObject( this._associatedObjectConfiguration, gd.ObjectJsImplementation ); const placeholder = instance.getRawStringProperty('placeholder') || object.content.placeholder; const initialValue = instance.getRawStringProperty('initialValue') || object.content.initialValue; const hasInitialValue = initialValue !== ''; this._pixiText.text = hasInitialValue ? initialValue : placeholder; const textColor = object.content.textColor; const finalTextColor = hasInitialValue ? objectsRenderingService.rgbOrHexToHexNumber(textColor) : 0x888888; if (this._finalTextColor !== finalTextColor) { this._finalTextColor = finalTextColor; this._pixiText.style.fill = finalTextColor; this._pixiText.dirty = true; } const fontSize = object.content.fontSize; if (this._pixiText.style.fontSize !== fontSize) { this._pixiText.style.fontSize = fontSize; this._pixiText.dirty = true; } const fontResourceName = object.content.fontResourceName; if (this._fontResourceName !== fontResourceName) { this._fontResourceName = fontResourceName; this._pixiResourcesLoader .loadFontFamily(this._project, fontResourceName) .then((fontFamily) => { if (this._wasDestroyed) return; this._pixiText.style.fontFamily = fontFamily; this._pixiText.dirty = true; }) .catch((err) => { // Ignore errors console.warn( 'Unable to load font family for RenderedTextInputObjectInstance', err ); }); } // Position the object. let width = DEFAULT_WIDTH; let height = DEFAULT_HEIGHT; if (instance.hasCustomSize()) { width = this.getCustomWidth(); height = this.getCustomHeight(); } this._pixiObject.pivot.x = width / 2; this._pixiObject.pivot.y = height / 2; this._pixiObject.position.x = instance.getX() + width / 2; this._pixiObject.position.y = instance.getY() + height / 2; this._pixiObject.rotation = RenderedInstance.toRad( this._instance.getAngle() ); const borderWidth = object.content.borderWidth || 0; const paddingX = object.content.paddingX !== undefined ? Math.min( parseFloat(object.content.paddingX), width / 2 - borderWidth ) : 2; const paddingY = object.content.paddingY !== undefined ? Math.min( parseFloat(object.content.paddingY), height / 2 - borderWidth ) : 1; // Draw the mask for the text. const textOffsetX = borderWidth + paddingX; // textOffsetY does not include the paddingY because browsers display the text, even if it is supposed to be cut off by vertical padding. const textOffsetY = borderWidth; this._pixiTextMask.clear(); this._pixiTextMask.beginFill(0xdddddd, 1); this._pixiTextMask.drawRect( textOffsetX, textOffsetY, width - 2 * textOffsetX, height - 2 * textOffsetY ); this._pixiTextMask.endFill(); const isTextArea = object.content.inputType === 'text area'; const textAlign = object.content.textAlign ? object.content.textAlign : 'left'; if (textAlign === 'left') this._pixiText.position.x = textOffsetX; else if (textAlign === 'right') this._pixiText.position.x = 0 + width - this._pixiText.width - textOffsetX; else if (textAlign === 'center') { this._pixiText.align = 'center'; this._pixiText.position.x = 0 + width / 2 - this._pixiText.width / 2; } this._pixiText.position.y = isTextArea ? textOffsetY + paddingY : height / 2 - this._pixiText.height / 2; // Draw the background and border. const fillColor = object.content.fillColor; const fillOpacity = object.content.fillOpacity; const borderColor = object.content.borderColor; const borderOpacity = object.content.borderOpacity; this._pixiGraphics.clear(); this._pixiGraphics.lineStyle( borderWidth, objectsRenderingService.rgbOrHexToHexNumber(borderColor), borderOpacity / 255 ); this._pixiGraphics.beginFill( objectsRenderingService.rgbOrHexToHexNumber(fillColor), fillOpacity / 255 ); this._pixiGraphics.drawRect(0, 0, width, height); this._pixiGraphics.endFill(); // Do not hide completely an object so it can still be manipulated const alphaForDisplay = Math.max( this._instance.getOpacity() / 255, 0.5 ); this._pixiObject.alpha = alphaForDisplay; } getDefaultWidth() { return DEFAULT_WIDTH; } getDefaultHeight() { return DEFAULT_HEIGHT; } } objectsRenderingService.registerInstanceRenderer( 'TextInput::TextInputObject', RenderedTextInputObjectInstance ); }, };