Files
GDevelop/Extensions/TextInput/JsExtension.js
Florian Rival 50326e4d21 Add missing Text Input to the web-app (#3785)
Don't show in changelog
2022-03-29 13:17:44 +02:00

745 lines
23 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @flow
/**
* 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
*/
/*::
// Import types to allow Flow to do static type checking on this file.
// Extensions declaration are typed using Flow (like the editor), but the files
// for the game engine are checked with TypeScript annotations.
import { type ObjectsRenderingService, type ObjectsEditorService } from '../JsExtensionTypes.flow.js'
*/
module.exports = {
createExtension: function (
_ /*: (string) => string */,
gd /*: libGDevelop */
) {
const extension = new gd.PlatformExtension();
extension.setExtensionInformation(
'TextInput',
_('Text Input'),
_('A text field the player can type text into.'),
'Florian Rival',
'MIT'
);
const textInputObject = new gd.ObjectJsImplementation();
// $FlowExpectedError - ignore Flow warning as we're creating an object
textInputObject.updateProperty = function (
objectContent,
propertyName,
newValue
) {
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') {
objectContent.inputType = newValue;
return true;
} 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;
}
return false;
};
// $FlowExpectedError - ignore Flow warning as we're creating an object
textInputObject.getProperties = function (objectContent) {
const objectProperties = new gd.MapStringPropertyDescriptor();
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')
.addExtraInfo('text')
.addExtraInfo('text area')
.addExtraInfo('email')
.addExtraInfo('password')
.addExtraInfo('number')
.addExtraInfo('telephone number')
.addExtraInfo('url')
.addExtraInfo('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 appearance'));
objectProperties
.getOrCreate('disabled')
.setValue(objectContent.disabled ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Disabled'))
.setGroup(_('Field appearance'));
objectProperties
.getOrCreate('textColor')
.setValue(objectContent.textColor || '0;0;0')
.setType('color')
.setLabel(_('Text color'))
.setGroup(_('Field appearance'));
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(_('Border color'))
.setGroup(_('Field appearance'));
objectProperties
.getOrCreate('borderOpacity')
.setValue(
(objectContent.borderOpacity != undefined
? objectContent.borderOpacity
: 255
).toString()
)
.setType('number')
.setLabel(_('Border opacity'))
.setGroup(_('Field appearance'));
objectProperties
.getOrCreate('borderWidth')
.setValue((objectContent.borderWidth || 0).toString())
.setType('number')
.setLabel(_('Border width'))
.setGroup(_('Field appearance'));
return objectProperties;
};
textInputObject.setRawJSONContent(
JSON.stringify({
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,
})
);
// $FlowExpectedError - ignore Flow warning as we're creating an object
textInputObject.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue,
project,
layout
) {
if (propertyName === 'initialValue') {
instance.setRawStringProperty('initialValue', newValue);
return true;
} else if (propertyName === 'placeholder') {
instance.setRawStringProperty('placeholder', newValue);
return true;
}
return false;
};
// $FlowExpectedError - ignore Flow warning as we're creating an object
textInputObject.getInitialInstanceProperties = function (
content,
instance,
project,
layout
) {
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 (experimental)'),
_('A text field the player can type text into.'),
'JsPlatform/Extensions/text_input.svg',
textInputObject
)
.setCategoryFullName(_('Form control'))
.addUnsupportedBaseObjectCapability('effect')
.setIncludeFile('Extensions/TextInput/textinputruntimeobject.js')
.addIncludeFile(
'Extensions/TextInput/textinputruntimeobject-pixi-renderer.js'
);
// Properties expressions/conditions/actions:
object
.addExpressionAndConditionAndAction(
'string',
'Text',
_('Text'),
_('the text'),
_('the text'),
'',
'res/conditions/text24.png'
)
.addParameter('object', _('Text input'), 'TextInputObject', false)
.useStandardParameters('string')
.setFunctionName('setString')
.setGetter('getString');
object
.addExpressionAndConditionAndAction(
'string',
'Placeholder',
_('Placeholder'),
_('the placeholder'),
_('the placeholder'),
'',
'res/conditions/text24.png'
)
.addParameter('object', _('Text input'), 'TextInputObject', false)
.useStandardParameters('string')
.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')
.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')
.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', _('Bitmap text'), '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.png'
)
.addParameter('object', _('Text input'), 'TextInputObject', false)
.useStandardParameters('string') // TODO: stringWithSelector?
.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')
.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')
.setFunctionName('setBorderOpacity')
.setGetter('getBorderOpacity');
object
.addExpressionAndConditionAndAction(
'number',
'BorderWidth',
_('Border width'),
_('the border width'),
_('the border width'),
_('Field appearance'),
'res/conditions/outlineSize24.png'
)
.addParameter('object', _('Text input'), 'TextInputObject', false)
.useStandardParameters('number')
.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.png'
)
.addParameter('object', _('Text input'), 'TextInputObject', false)
.useStandardParameters('boolean')
.setFunctionName('setReadOnly')
.setGetter('isReadOnly');
object
.addExpressionAndConditionAndAction(
'boolean',
'Disabled',
_('Disabled'),
_('the text input is disabled'),
_('disabled'),
'',
'res/conditions/text24.png'
)
.addParameter('object', _('Text input'), 'TextInputObject', false)
.useStandardParameters('boolean')
.setFunctionName('setDisabled')
.setGetter('isDisabled');
// Other expressions/conditions/actions:
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')
.setFunctionName('setOpacity')
.setGetter('getOpacity');
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');
return extension;
},
/**
* You can optionally add sanity tests that will check the basic working
* of your extension behaviors/objects by instanciating 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 /*: libGDevelop */,
extension /*: gdPlatformExtension*/
) {
return [];
},
/**
* Register editors for objects.
*
* Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
*/
registerEditorConfigurations: function (
objectsEditorService /*: 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 /*: ObjectsRenderingService */
) {
const RenderedInstance = objectsRenderingService.RenderedInstance;
const PIXI = objectsRenderingService.PIXI;
const DEFAULT_WIDTH = 300;
const DEFAULT_HEIGHT = 30;
const TEXT_MASK_PADDING = 2;
class RenderedTextInputObjectInstance extends RenderedInstance {
constructor(
project,
layout,
instance,
associatedObject,
pixiContainer,
pixiResourcesLoader
) {
super(
project,
layout,
instance,
associatedObject,
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();
}
static getThumbnail(project, resourcesLoader, object) {
return 'JsPlatform/Extensions/text_input.svg';
}
update() {
const instance = this._instance;
const properties = this._associatedObject.getProperties();
const placeholder =
instance.getRawStringProperty('placeholder') ||
properties.get('placeholder').getValue();
const initialValue =
instance.getRawStringProperty('initialValue') ||
properties.get('initialValue').getValue();
const hasInitialValue = initialValue !== '';
this._pixiText.text = hasInitialValue ? initialValue : placeholder;
const textColor = properties.get('textColor').getValue();
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 = parseFloat(properties.get('fontSize').getValue());
if (this._pixiText.style.fontSize !== fontSize) {
this._pixiText.style.fontSize = fontSize;
this._pixiText.dirty = true;
}
const fontResourceName = properties.get('fontResourceName').getValue();
if (this._fontResourceName !== fontResourceName) {
this._fontResourceName = fontResourceName;
this._pixiResourcesLoader
.loadFontFamily(this._project, fontResourceName)
.then((fontFamily) => {
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 = instance.getCustomWidth();
height = instance.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 =
parseFloat(properties.get('borderWidth').getValue()) || 0;
// Draw the mask for the text.
const textOffset = borderWidth + TEXT_MASK_PADDING;
this._pixiTextMask.clear();
this._pixiTextMask.beginFill(0xdddddd, 1);
this._pixiTextMask.drawRect(
textOffset,
textOffset,
width - 2 * textOffset,
height - 2 * textOffset
);
this._pixiTextMask.endFill();
const isTextArea =
properties.get('inputType').getValue() === 'text area';
this._pixiText.position.x = textOffset;
this._pixiText.position.y = isTextArea
? textOffset
: height / 2 - this._pixiText.height / 2;
// Draw the background and border.
const fillColor = properties.get('fillColor').getValue();
const fillOpacity = parseFloat(
properties.get('fillOpacity').getValue()
);
const borderColor = properties.get('borderColor').getValue();
const borderOpacity = parseFloat(
properties.get('borderOpacity').getValue()
);
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();
}
getDefaultWidth() {
return DEFAULT_WIDTH;
}
getDefaultHeight() {
return DEFAULT_HEIGHT;
}
}
objectsRenderingService.registerInstanceRenderer(
'TextInput::TextInputObject',
RenderedTextInputObjectInstance
);
},
};