diff --git a/index.html b/index.html index 2c4ddc47..9702fbb2 100644 --- a/index.html +++ b/index.html @@ -9,8 +9,7 @@
- - + diff --git a/package-lock.json b/package-lock.json index 21fc33b8..60727e84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5295,6 +5295,12 @@ } } }, + "es6-promisify": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.0.1.tgz", + "integrity": "sha512-J3ZkwbEnnO+fGAKrjVpeUAnZshAdfZvbhQpqfIH9kSAspReRC4nJnu8ewm55b4y9ElyeuhCTzJD0XiH8Tsbhlw==", + "dev": true + }, "es6-symbol": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", @@ -18879,6 +18885,17 @@ } } }, + "webpack-merge-and-include-globally": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/webpack-merge-and-include-globally/-/webpack-merge-and-include-globally-2.1.16.tgz", + "integrity": "sha512-ZhPgMbQFB5bK1ZvtDJm75Lu2BHEm9I5iDbSa3WM4y/VHa0SlLMiBs4FiJzLeJWa9dXL5sVhws/0ebRSlPfqsvA==", + "dev": true, + "requires": { + "es6-promisify": "^6.0.0", + "glob": "^7.1.2", + "rev-hash": "^2.0.0" + } + }, "webpack-sources": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", diff --git a/package.json b/package.json index 592acdf4..a99ff342 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,8 @@ "vinyl-paths": "^2.1.0", "webpack": "^4.32.2", "webpack-cli": "^3.3.2", - "webpack-dev-server": "^3.4.1" + "webpack-dev-server": "^3.4.1", + "webpack-merge-and-include-globally": "^2.1.16" }, "dependencies": { "@binary-com/smartcharts": "^0.3.9", diff --git a/src/assets/sass/scratch/_blockly-toolbox.scss b/src/assets/sass/scratch/_blockly-toolbox.scss index 395e9e72..b4c024a3 100644 --- a/src/assets/sass/scratch/_blockly-toolbox.scss +++ b/src/assets/sass/scratch/_blockly-toolbox.scss @@ -52,7 +52,6 @@ } .blocklyToolboxDiv { - background-color: $brand-gray !important; border-width: thin; color: $brand-dark-gray; border-right: 0.06em solid; @@ -84,6 +83,7 @@ } .blocklySvg { + background-color: $white !important; position: absolute; } diff --git a/src/scratch/blocks/Advanced/Functions/index.js b/src/scratch/blocks/Advanced/Functions/index.js new file mode 100755 index 00000000..abd17164 --- /dev/null +++ b/src/scratch/blocks/Advanced/Functions/index.js @@ -0,0 +1,5 @@ +import './procedures_defnoreturn'; +import './procedures_defreturn'; +import './procedures_callnoreturn'; +import './procedures_callreturn'; +import './procedures_ifreturn'; diff --git a/src/scratch/blocks/Advanced/Functions/procedures_callnoreturn.js b/src/scratch/blocks/Advanced/Functions/procedures_callnoreturn.js new file mode 100755 index 00000000..4cc6a83e --- /dev/null +++ b/src/scratch/blocks/Advanced/Functions/procedures_callnoreturn.js @@ -0,0 +1,338 @@ +import { setBlockTextColor } from '../../../utils'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.procedures_callnoreturn = { + init() { + this.arguments = []; + this.argumentVarModels = []; + this.previousDisabledState = false; + + this.jsonInit({ + message0: '%1 %2', + args0 : [ + { + type: 'field_label', + name: 'NAME', + text: this.id, + }, + { + type: 'input_dummy', + name: 'TOPROW', + }, + ], + colour : Blockly.Colours.BinaryProcedures.colour, + colourSecondary : Blockly.Colours.BinaryProcedures.colourSecondary, + colourTertiary : Blockly.Colours.BinaryProcedures.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, + /** + * Procedure calls cannot exist without the corresponding procedure + * definition. Enforce this link whenever an event is fired. + * @param {!Blockly.Events.Abstract} event Change event. + * @this Blockly.Block + */ + onchange(event) { + setBlockTextColor(this); + + if (!this.workspace || this.workspace.isFlyout) { + // Block is deleted or is in a flyout. + return; + } + + if (!event.recordUndo) { + // Events not generated by user. Skip handling. + return; + } + + if (event.type === Blockly.Events.BLOCK_CREATE && event.ids.indexOf(this.id) !== -1) { + // Look for the case where a procedure call was created (usually through + // paste) and there is no matching definition. In this case, create + // an empty definition block with the correct signature. + const name = this.getProcedureCall(); + let def = this.getProcedureDefinition(name); + + // Set data of `this` block to 'procedure definition'-block `id` so we can keep track of their relation. + if (!def) { + this.unplug(); + this.dispose(); + } + + this.data = def.id; + + if ( + def && + (def.type !== this.defType || JSON.stringify(def.arguments) !== JSON.stringify(this.arguments)) + ) { + // The signatures don't match. + def = null; + } + if (!def) { + Blockly.Events.setGroup(event.group); + /** + * Create matching definition block. + * + * + * + * + * + * test + * + * + */ + const xml = document.createElement('xml'); + const block = document.createElement('block'); + block.setAttribute('type', this.defType); + + const xy = this.getRelativeToSurfaceXY(); + const x = xy.x + Blockly.SNAP_RADIUS * (this.RTL ? -1 : 1); + const y = xy.y + Blockly.SNAP_RADIUS * 2; + + block.setAttribute('x', x); + block.setAttribute('y', y); + + const mutation = this.mutationToDom(); + + block.appendChild(mutation); + + const field = document.createElement('field'); + field.setAttribute('name', 'NAME'); + field.appendChild(document.createTextNode(this.getProcedureCall())); + + block.appendChild(field); + xml.appendChild(block); + + Blockly.Xml.domToWorkspace(xml, this.workspace); + Blockly.Events.setGroup(false); + } + } else if (event.type === Blockly.Events.BLOCK_DELETE) { + // Look for the case where a procedure definition has been deleted, + // leaving this block (a procedure call) orphaned. In this case, delete + // the orphan. + const name = this.getProcedureCall(); + const def = Blockly.Procedures.getDefinition(name, this.workspace); + + if (!def) { + Blockly.Events.setGroup(event.group); + this.dispose(true, false); + Blockly.Events.setGroup(false); + } + } else if (event.type === Blockly.Events.BLOCK_CHANGE && event.element === 'disabled') { + const name = this.getProcedureCall(); + const def = Blockly.Procedures.getDefinition(name, this.workspace); + + if (def && def.id === event.blockId) { + // in most cases the old group should be '' + const oldGroup = Blockly.Events.getGroup(); + + if (oldGroup) { + // This should only be possible programatically and may indicate a problem + // with event grouping. If you see this message please investigate. If the + // use ends up being valid we may need to reorder events in the undo stack. + // eslint-disable-next-line no-console + console.log('Saw an existing group while responding to a definition change'); + } + + Blockly.Events.setGroup(event.group); + + if (event.newValue) { + this.previousDisabledState = this.disabled; + this.setDisabled(true); + } else { + this.setDisabled(this.previousDisabledState); + } + + Blockly.Events.setGroup(oldGroup); + } + } + }, + /** + * Returns the related procedure definition block. + * @return {Blockly.Block} Procedure definition block. + * @this Blockly.Block + */ + getProcedureDefinition(name) { + // Assume that a procedure definition is a top block. + return this.workspace.getTopBlocks(false).find(block => { + if (block.getProcedureDef) { + const tuple = block.getProcedureDef(); + return tuple && Blockly.Names.equals(tuple[0], name); + } + return false; + }); + }, + /** + * Returns the name of the procedure this block calls. + * @return {string} Procedure name. + * @this Blockly.Block + */ + getProcedureCall() { + // The NAME field is guaranteed to exist, null will never be returned. + return /** @type {string} */ (this.getFieldValue('NAME')); + }, + /** + * Notification that a procedure is renaming. + * If the name matches this block's procedure, rename it. + * @param {string} oldName Previous name of procedure. + * @param {string} newName Renamed procedure. + * @this Blockly.Block + */ + renameProcedure(oldName, newName) { + if (Blockly.Names.equals(oldName, this.getProcedureCall())) { + this.setFieldValue(newName, 'NAME'); + } + }, + /** + * Notification that the procedure's parameters have changed. + * @param {!Array.} paramNames New param names, e.g. ['x', 'y', 'z']. + * @private + * @this Blockly.Block + */ + setProcedureParameters(paramNames) { + // Rebuild the block's arguments. + this.arguments = [].concat(paramNames); + + // And rebuild the argument model list. + this.argumentVarModels = this.arguments.map(argumentName => + Blockly.Variables.getOrCreateVariablePackage(this.workspace, null, argumentName, '') + ); + + this.updateShape(); + }, + /** + * Modify this block to have the correct number of arguments. + * @private + * @this Blockly.Block + */ + updateShape() { + this.arguments.forEach((argumentName, i) => { + let field = this.getField(`ARGNAME${i}`); + if (field) { + // Ensure argument name is up to date. + // The argument name field is deterministic based on the mutation, + // no need to fire a change event. + Blockly.Events.disable(); + try { + field.setValue(argumentName); + } finally { + Blockly.Events.enable(); + } + } else { + // Add new input. + field = new Blockly.FieldLabel(argumentName); + const input = this.appendValueInput(`ARG${i}`).appendField(field, `ARGNAME${i}`); + input.init(); + } + }); + + // Remove deleted inputs. + let i = this.arguments.length; + while (this.getInput(`ARG${i}`)) { + this.removeInput(`ARG${i}`); + i++; + } + + // Add 'with:' if there are parameters, remove otherwise. + const topRow = this.getInput('TOPROW'); + + if (topRow) { + if (this.arguments.length) { + if (!this.getField('WITH')) { + topRow.appendField(translate('with:'), 'WITH'); + topRow.init(); + } + } else if (this.getField('WITH')) { + topRow.removeField('WITH'); + } + } + }, + /** + * Create XML to represent the (non-editable) name and arguments. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom() { + const container = document.createElement('mutation'); + container.setAttribute('name', this.getProcedureCall()); + + this.arguments.forEach(argumentName => { + const parameter = document.createElement('arg'); + parameter.setAttribute('name', argumentName); + container.appendChild(parameter); + }); + + return container; + }, + /** + * Parse XML to restore the (non-editable) name and parameters. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation(xmlElement) { + const name = xmlElement.getAttribute('name'); + this.renameProcedure(this.getProcedureCall(), name); + + const args = []; + const paramIds = []; + + xmlElement.childNodes.forEach(childNode => { + if (childNode.nodeName.toLowerCase() === 'arg') { + args.push(childNode.getAttribute('name')); + paramIds.push(childNode.getAttribute('paramId')); + } + }); + + this.setProcedureParameters(args, paramIds); + }, + /** + * Return all variables referenced by this block. + * @return {!Array.} List of variable models. + * @this Blockly.Block + */ + getVarModels() { + return this.argumentVarModels; + }, + /** + * Add menu option to find the definition block for this call. + * @param {!Array} options List of menu options to add to. + * @this Blockly.Block + */ + customContextMenu(options) { + if (!this.workspace.isMovable()) { + // If we center on the block and the workspace isn't movable we could + // loose blocks at the edges of the workspace. + return; + } + + const name = this.getProcedureCall(); + const { workspace } = this; + + const option = { enabled: true }; + option.text = translate('Highlight function definition'); + option.callback = () => { + const def = this.getProcedureDefinition(name); + if (def) { + workspace.centerOnBlock(def.id); + def.select(); + } + }; + + options.push(option); + }, + defType: 'procedures_defnoreturn', +}; + +Blockly.JavaScript.procedures_callnoreturn = block => { + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('NAME'), + Blockly.Procedures.NAME_TYPE + ); + const args = block.arguments.map( + (arg, i) => Blockly.JavaScript.valueToCode(block, `ARG${i}`, Blockly.JavaScript.ORDER_COMMA) || 'null' + ); + + const code = `${functionName}(${args.join(', ')});\n`; + return code; +}; diff --git a/src/scratch/blocks/Advanced/Functions/procedures_callreturn.js b/src/scratch/blocks/Advanced/Functions/procedures_callreturn.js new file mode 100755 index 00000000..9118dcd7 --- /dev/null +++ b/src/scratch/blocks/Advanced/Functions/procedures_callreturn.js @@ -0,0 +1,50 @@ +Blockly.Blocks.procedures_callreturn = { + init() { + this.arguments = []; + this.previousDisabledState = false; + + this.jsonInit({ + message0: '%1 %2', + args0 : [ + { + type: 'field_label', + name: 'NAME', + text: this.id, + }, + { + type: 'input_dummy', + name: 'TOPROW', + }, + ], + output : null, + colour : Blockly.Colours.BinaryProcedures.colour, + colourSecondary: Blockly.Colours.BinaryProcedures.colourSecondary, + colourTertiary : Blockly.Colours.BinaryProcedures.colourTertiary, + }); + }, + onchange : Blockly.Blocks.procedures_callnoreturn.onchange, + getProcedureDefinition: Blockly.Blocks.procedures_callnoreturn.getProcedureDefinition, + getProcedureCall : Blockly.Blocks.procedures_callnoreturn.getProcedureCall, + renameProcedure : Blockly.Blocks.procedures_callnoreturn.renameProcedure, + setProcedureParameters: Blockly.Blocks.procedures_callnoreturn.setProcedureParameters, + updateShape : Blockly.Blocks.procedures_callnoreturn.updateShape, + mutationToDom : Blockly.Blocks.procedures_callnoreturn.mutationToDom, + domToMutation : Blockly.Blocks.procedures_callnoreturn.domToMutation, + getVarModels : Blockly.Blocks.procedures_callnoreturn.getVarModels, + customContextMenu : Blockly.Blocks.procedures_callnoreturn.customContextMenu, + defType : 'procedures_defreturn', +}; + +Blockly.JavaScript.procedures_callreturn = block => { + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('NAME'), + Blockly.Procedures.NAME_TYPE + ); + const args = block.arguments.map( + (arg, i) => Blockly.JavaScript.valueToCode(block, `ARG${i}`, Blockly.JavaScript.ORDER_COMMA) || 'null' + ); + + const code = `${functionName}(${args.join(', ')})`; + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Advanced/Functions/procedures_defnoreturn.js b/src/scratch/blocks/Advanced/Functions/procedures_defnoreturn.js new file mode 100755 index 00000000..6d6c7239 --- /dev/null +++ b/src/scratch/blocks/Advanced/Functions/procedures_defnoreturn.js @@ -0,0 +1,329 @@ +import { plusIconLight } from '../../images'; +import { setBlockTextColor } from '../../../utils'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.procedures_defnoreturn = { + init() { + this.arguments = []; + this.argumentVarModels = []; + + this.jsonInit({ + message0: translate('function %1 %2'), + args0 : [ + { + type: 'field_input', + name: 'NAME', + text: '', + }, + { + type: 'field_label', + name: 'PARAMS', + text: '', + }, + ], + colour : Blockly.Colours.BinaryProcedures.colour, + colourSecondary: Blockly.Colours.BinaryProcedures.colourSecondary, + colourTertiary : Blockly.Colours.BinaryProcedures.colourTertiary, + }); + + // Enforce unique procedure names + const nameField = this.getField('NAME'); + nameField.setValidator(Blockly.Procedures.rename); + + // Render a ➕-icon for adding parameters + const fieldImage = new Blockly.FieldImage(plusIconLight, 24, 24, '+', () => this.onAddClick()); + this.appendDummyInput('ADD_ICON').appendField(fieldImage); + + this.setStatements(true); + }, + /** + * Sets the block colour and updates this procedure's caller blocks + * to reflect the same name on a change. + * @param {!Blockly.Events.Abstract} event Change event. + * @this Blockly.Block + */ + onchange(event) { + setBlockTextColor(this); + + const allowedEvents = [Blockly.Events.BLOCK_DELETE, Blockly.Events.BLOCK_CREATE, Blockly.Events.BLOCK_CHANGE]; + if (!this.workspace || this.workspace.isFlyout || !allowedEvents.includes(event.type)) { + return; + } + + if (event.type === Blockly.Events.BLOCK_CHANGE) { + // Sync names between definition- and execution-block + if (event.blockId === this.id && event.name === 'NAME') { + this.getProcedureCallers().forEach(block => { + block.setFieldValue(event.newValue, 'NAME'); + }); + } + } + }, + /** + * Prompt the user for parameter name + * @this Blockly.Block + */ + onAddClick() { + // Wrap in setTimeout so block doesn't stick to mouse (Blockly.Events.END_DRAG event isn't blocked). + setTimeout(() => { + const promptMessage = translate('Specify a parameter name:'); + Blockly.prompt(promptMessage, '', paramName => { + if (paramName) { + const variable = Blockly.Variables.getOrCreateVariablePackage(this.workspace, null, paramName, ''); + if (variable) { + this.arguments.push(paramName); + this.argumentVarModels.push(variable); + + const paramField = this.getField('PARAMS'); + paramField.setText(`${translate('with: ')} ${this.arguments.join(', ')}`); + + this.getProcedureCallers().forEach(block => { + block.setProcedureParameters(this.arguments); + block.initSvg(); + block.render(false); + }); + } + } + }); + }, 200); + }, + /** + * Add or remove the statement block from this function definition. + * @param {boolean} hasStatements True if a statement block is needed. + * @this Blockly.Block + */ + setStatements(hasStatements) { + if (this.hasStatements === hasStatements) { + return; + } + + if (hasStatements) { + this.appendStatementInput('STACK').appendField(''); + if (this.getInput('RETURN')) { + this.moveInputBefore('STACK', 'RETURN'); + } + } else { + this.removeInput('STACK', true); + } + + this.hasStatements = hasStatements; + }, + /** + * Update the display of parameters for this procedure definition block. + * @private + * @this Blockly.Block + */ + updateParams() { + let paramString = ''; + + if (this.arguments.length) { + paramString = `${translate('with:')} ${this.arguments.join(', ')}`; + } + + // The params field is deterministic based on the mutation, + // no need to fire a change event. + Blockly.Events.disable(); + try { + this.setFieldValue(paramString, 'PARAMS'); + } finally { + Blockly.Events.enable(); + } + }, + /** + * Create XML to represent the argument inputs. + * @param {boolean=} optParamIds If true include the IDs of the parameter + * quarks. Used by Blockly.Procedures.mutateCallers for reconnection. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom(optParamIds) { + const container = document.createElement('mutation'); + + if (optParamIds) { + container.setAttribute('name', this.getFieldValue('NAME')); + } + + this.argumentVarModels.forEach((arg, i) => { + const parameter = document.createElement('arg'); + + parameter.setAttribute('name', arg.name); + parameter.setAttribute('varid', arg.getId()); + + if (optParamIds && this.paramIds) { + parameter.setAttribute('paramId', this.paramIds[i]); + } + container.appendChild(parameter); + }); + + // Save whether the statement input is visible. + if (!this.hasStatements) { + container.setAttribute('statements', 'false'); + } + + return container; + }, + /** + * Parse XML to restore the argument inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation(xmlElement) { + this.arguments = []; + this.argumentVarModels = []; + + xmlElement.childNodes.forEach(childNode => { + if (childNode.nodeName.toLowerCase() === 'arg') { + const varName = childNode.getAttribute('name'); + this.arguments.push(varName); + + const varId = childNode.getAttribute('varid') || childNode.getAttribute('varId'); + const variable = Blockly.Variables.getOrCreateVariablePackage(this.workspace, varId, varName, ''); + + if (variable !== null) { + this.argumentVarModels.push(variable); + } else { + // eslint-disable-next-line no-console + console.log(`Failed to create a variable with name ${varName}, ignoring.`); + } + } + }); + + this.updateParams(); + + // Show or hide the statement input. + this.setStatements(xmlElement.getAttribute('statements') !== 'false'); + }, + /** + * Return the signature of this procedure definition. + * @return {!Array} Tuple containing three elements: + * - the name of the defined procedure, + * - a list of all its arguments, + * - that it DOES NOT have a return value. + * @this Blockly.Block + */ + getProcedureDef() { + return [this.getFieldValue('NAME'), this.arguments, false]; + }, + /** + * Return all procedure callers related to this block. + * @return {!Array.} List of procedure caller blocks + * @this Blockly.Block + */ + getProcedureCallers() { + return this.workspace + .getAllBlocks(false) + .filter(block => block.type === this.callType && block.data === this.id); + }, + /** + * Return all variables referenced by this block. + * @return {!Array.} List of variable names. + * @this Blockly.Block + */ + getVars() { + return this.arguments; + }, + /** + * Return all variables referenced by this block. + * @return {!Array.} List of variable models. + * @this Blockly.Block + */ + getVarModels() { + return this.argumentVarModels; + }, + /** + * Add custom menu options to this block's context menu. + * @param {!Array} options List of menu options to add to. + * @this Blockly.Block + */ + customContextMenu(options) { + if (this.isInFlyout) { + return; + } + // Add option to create caller. + const option = { enabled: true }; + const name = this.getFieldValue('NAME'); + option.text = Blockly.Msg.PROCEDURES_CREATE_DO.replace('%1', name); + + const xmlMutation = document.createElement('mutation'); + xmlMutation.setAttribute('name', name); + + this.arguments.forEach(argumentName => { + const xmlArg = document.createElement('arg'); + xmlArg.setAttribute('name', argumentName); + xmlMutation.appendChild(xmlArg); + }); + + const xmlBlock = document.createElement('block'); + xmlBlock.setAttribute('type', this.callType); + xmlBlock.appendChild(xmlMutation); + option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); + options.push(option); + + // Add options to create getters for each parameter. + if (!this.isCollapsed()) { + this.argumentVarModels.forEach(argumentVarModel => { + const getOption = { enabled: true }; + + getOption.text = Blockly.Msg.VARIABLES_SET_CREATE_GET.replace('%1', argumentVarModel.name); + + const xmlField = Blockly.Variables.generateVariableFieldDom(argumentVarModel); + const xmlOptionBlock = document.createElement('block'); + + xmlOptionBlock.setAttribute('type', 'variables_get'); + xmlOptionBlock.appendChild(xmlField); + + getOption.callback = Blockly.ContextMenu.callbackFactory(this, xmlOptionBlock); + options.push(getOption); + }); + } + }, + callType: 'procedures_callnoreturn', +}; + +Blockly.JavaScript.procedures_defnoreturn = block => { + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('NAME'), + Blockly.Procedures.NAME_TYPE + ); + + let branch = Blockly.JavaScript.statementToCode(block, 'STACK'); + + if (Blockly.JavaScript.STATEMENT_PREFIX) { + const id = block.id.replace(/\$/g, '$$$$'); // Issue 251. + + branch = + Blockly.JavaScript.prefixLines( + Blockly.JavaScript.STATEMENT_PREFIX.replace(/%1/g, `'${id}'`), + Blockly.JavaScript.INDENT + ) + branch; + } + + if (Blockly.JavaScript.INFINITE_LOOP_TRAP) { + branch = Blockly.JavaScript.INFINITE_LOOP_TRAP.replace(/%1/g, `'${block.id}'`) + branch; + } + + let returnValue = Blockly.JavaScript.valueToCode(block, 'RETURN', Blockly.JavaScript.ORDER_NONE) || ''; + if (returnValue) { + returnValue = `${Blockly.JavaScript.INDENT}return ${returnValue};\n`; + } + + const args = block.arguments.map( + argumentName => Blockly.JavaScript.variableDB_.getName(argumentName, Blockly.Variables.NAME_TYPE) // eslint-disable-line no-underscore-dangle + ); + + // eslint-disable-next-line no-underscore-dangle + const code = Blockly.JavaScript.scrub_( + block, + ` + function ${functionName}(${args.join(', ')}) { + ${branch} + ${returnValue} + }\n` + ); + + // Add % so as not to collide with helper functions in definitions list. + // eslint-disable-next-line no-underscore-dangle + Blockly.JavaScript.definitions_[`%${functionName}`] = code; + return null; +}; diff --git a/src/scratch/blocks/Advanced/Functions/procedures_defreturn.js b/src/scratch/blocks/Advanced/Functions/procedures_defreturn.js new file mode 100755 index 00000000..4e175a50 --- /dev/null +++ b/src/scratch/blocks/Advanced/Functions/procedures_defreturn.js @@ -0,0 +1,79 @@ +import { plusIconDark } from '../../images'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.procedures_defreturn = { + init() { + this.arguments = []; + this.argumentVarModels = []; + + this.jsonInit({ + message0: translate('function %1 %2 %3'), + message1: 'return %1', + args0 : [ + { + type: 'field_input', + name: 'NAME', + text: '', + }, + { + type: 'field_label', + name: 'PARAMS', + text: '', + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_value', + name : 'RETURN', + check: null, + align: Blockly.ALIGN_RIGHT, + }, + ], + colour : Blockly.Colours.BinaryProcedures.colour, + colourSecondary: Blockly.Colours.BinaryProcedures.colourSecondary, + colourTertiary : Blockly.Colours.BinaryProcedures.colourTertiary, + }); + + // Enforce unique procedure names + const nameField = this.getField('NAME'); + nameField.setValidator(Blockly.Procedures.rename); + + // Render a ➕-icon for adding parameters + const fieldImage = new Blockly.FieldImage(plusIconDark, 24, 24, '+', () => this.onAddClick()); + this.appendDummyInput('ADD_ICON').appendField(fieldImage); + this.moveInputBefore('ADD_ICON', 'RETURN'); + + this.setStatements(true); + }, + onAddClick : Blockly.Blocks.procedures_defnoreturn.onAddClick, + onchange : Blockly.Blocks.procedures_defnoreturn.onchange, + setStatements: Blockly.Blocks.procedures_defnoreturn.setStatements, + updateParams : Blockly.Blocks.procedures_defnoreturn.updateParams, + mutationToDom: Blockly.Blocks.procedures_defnoreturn.mutationToDom, + domToMutation: Blockly.Blocks.procedures_defnoreturn.domToMutation, + /** + * Return the signature of this procedure definition. + * @return {!Array} Tuple containing three elements: + * - the name of the defined procedure, + * - a list of all its arguments, + * - that it DOES have a return value. + * @this Blockly.Block + */ + getProcedureDef() { + return [this.getFieldValue('NAME'), this.arguments, true]; + }, + getProcedureCallers : Blockly.Blocks.procedures_defnoreturn.getProcedureCallers, + getVars : Blockly.Blocks.procedures_defnoreturn.getVars, + getVarModels : Blockly.Blocks.procedures_defnoreturn.getVarModels, + renameVarById : Blockly.Blocks.procedures_defnoreturn.renameVarById, + updateVarName : Blockly.Blocks.procedures_defnoreturn.updateVarName, + displayRenamedVar : Blockly.Blocks.procedures_defnoreturn.displayRenamedVar, + customContextMenu : Blockly.Blocks.procedures_defnoreturn.customContextMenu, + callType : 'procedures_callreturn', + registerWorkspaceListener: Blockly.Blocks.procedures_defnoreturn.registerWorkspaceListener, +}; + +Blockly.JavaScript.procedures_defreturn = Blockly.JavaScript.procedures_defnoreturn; diff --git a/src/scratch/blocks/Advanced/Functions/procedures_ifreturn.js b/src/scratch/blocks/Advanced/Functions/procedures_ifreturn.js new file mode 100755 index 00000000..c8cca8fd --- /dev/null +++ b/src/scratch/blocks/Advanced/Functions/procedures_ifreturn.js @@ -0,0 +1,132 @@ +import { setBlockTextColor } from '../../../utils'; +import { translate } from '../../../../utils/lang/i18n'; + +/** + * Block for conditionally returning a value from a procedure. + * @this Blockly.Block + */ +Blockly.Blocks.procedures_ifreturn = { + init() { + this.hasReturnValue = true; + + this.jsonInit({ + message0: translate('if %1 return %2'), + args0 : [ + { + type: 'input_value', + name: 'CONDITION', + }, + { + type: 'input_value', + name: 'VALUE', + }, + ], + colour : Blockly.Colours.BinaryProcedures.colour, + colourSecondary : Blockly.Colours.BinaryProcedures.colourSecondary, + colourTertiary : Blockly.Colours.BinaryProcedures.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, + /** + * Create XML to represent whether this block has a return value. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom() { + const container = document.createElement('mutation'); + container.setAttribute('value', Number(this.hasReturnValue)); + return container; + }, + /** + * Parse XML to restore whether this block has a return value. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation(xmlElement) { + const value = xmlElement.getAttribute('value'); + this.hasReturnValue = value === '1'; + + if (!this.hasReturnValue) { + this.removeInput('VALUE'); + this.appendDummyInput('VALUE').appendField(translate('return')); + this.initSvg(); + this.render(); + } + }, + /** + * Called whenever anything on the workspace changes. + * Add warning if this flow block is not nested inside a loop. + * @param {!Blockly.Events.Abstract} e Change event. + * @this Blockly.Block + */ + onchange(/* e */) { + setBlockTextColor(this); + + if (!this.workspace.isDragging || this.workspace.isDragging()) { + return; // Don't change state at the start of a drag. + } + + let legal = false; + + // Is the block nested in a procedure? + let block = this; + do { + if (this.FUNCTION_TYPES.indexOf(block.type) !== -1) { + legal = true; + break; + } + block = block.getSurroundParent(); + } while (block); + + if (legal) { + const rerender = () => { + this.initSvg(); + this.render(); + }; + + // If needed, toggle whether this block has a return value. + if (block.type === 'procedures_defnoreturn' && this.hasReturnValue) { + this.removeInput('VALUE'); + this.appendDummyInput('VALUE').appendField(translate('return')); + rerender(); + this.hasReturnValue = false; + } else if (block.type === 'procedures_defreturn' && !this.hasReturnValue) { + this.removeInput('VALUE'); + this.appendValueInput('VALUE').appendField(translate('return')); + rerender(); + this.hasReturnValue = true; + } + + if (!this.isInFlyout) { + this.setDisabled(false); + } + } else if (!this.isInFlyout && !this.getInheritedDisabled()) { + this.setDisabled(true); + } + }, + /** + * List of block types that are functions and thus do not need warnings. + * To add a new function type add this to your code: + * Blockly.Blocks['procedures_ifreturn'].FUNCTION_TYPES.push('custom_func'); + */ + FUNCTION_TYPES: ['procedures_defnoreturn', 'procedures_defreturn'], +}; + +Blockly.JavaScript.procedures_ifreturn = block => { + const condition = Blockly.JavaScript.valueToCode(block, 'CONDITION', Blockly.JavaScript.ORDER_NONE) || 'false'; + + let branch; + if (block.hasReturnValue) { + const value = Blockly.JavaScript.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_NONE) || 'null'; + branch = `return ${value};\n`; + } else { + branch = 'return;\n'; + } + + const code = ` + if (${condition}) { + ${branch} + }\n`; + return code; +}; diff --git a/src/scratch/blocks/Advanced/List/index.js b/src/scratch/blocks/Advanced/List/index.js new file mode 100755 index 00000000..a0e7ed80 --- /dev/null +++ b/src/scratch/blocks/Advanced/List/index.js @@ -0,0 +1,11 @@ +import './lists_create_with'; +import './lists_repeat'; +import './lists_length'; +import './lists_isEmpty'; +import './lists_indexOf'; +import './lists_getIndex'; +import './lists_setIndex'; +import './lists_getSublist'; +import './lists_split'; +import './lists_sort'; +import './lists_statement'; diff --git a/src/scratch/blocks/Advanced/List/lists_create_with.js b/src/scratch/blocks/Advanced/List/lists_create_with.js new file mode 100755 index 00000000..cd2c2c82 --- /dev/null +++ b/src/scratch/blocks/Advanced/List/lists_create_with.js @@ -0,0 +1,87 @@ +import { plusIconDark } from '../../images'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.lists_create_with = { + init() { + this.jsonInit({ + message0: translate('set %1 to create list with'), + message1: '%1', + args0 : [ + { + type : 'field_variable', + name : 'VARIABLE', + variable: translate('list'), + }, + ], + args1: [ + { + type: 'input_statement', + name: 'STACK', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + // Render a ➕-icon for adding additional `lists_statement` blocks + const fieldImage = new Blockly.FieldImage(plusIconDark, 25, 25, '', () => this.onIconClick()); + this.appendDummyInput('ADD_ICON').appendField(fieldImage); + this.moveInputBefore('ADD_ICON', 'STACK'); + }, + onIconClick() { + if (!this.workspace || this.isInFlyout) { + return; + } + + const statementBlock = this.workspace.newBlock('lists_statement'); + statementBlock.requiredParentId = this.id; + statementBlock.setMovable(false); + statementBlock.initSvg(); + statementBlock.render(); + + const connection = this.getLastConnectionInStatement('STACK'); + connection.connect(statementBlock.previousConnection); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + // Only allow `lists_statement` blocks to be part of the `STACK` + let currentBlock = this.getInputTargetBlock('STACK'); + while (currentBlock !== null) { + if (currentBlock.type !== 'lists_statement') { + currentBlock.unplug(false); + } + currentBlock = currentBlock.getNextBlock(); + } + } + }, +}; + +Blockly.JavaScript.lists_create_with = block => { + const variable = block.getFieldValue('VARIABLE'); + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName(variable, Blockly.Variables.NAME_TYPE); + const elements = []; + + let currentBlock = block.getInputTargetBlock('STACK'); + while (currentBlock !== null) { + const value = Blockly.JavaScript[currentBlock.type](currentBlock); + + if (Array.isArray(value) && value.length === 2) { + elements.push(value[0]); + } else { + elements.push(value); + } + + currentBlock = currentBlock.getNextBlock(); + } + + const code = `${varName} = [${elements.join(', ')}];\n`; + return code; +}; diff --git a/src/scratch/blocks/Advanced/List/lists_getIndex.js b/src/scratch/blocks/Advanced/List/lists_getIndex.js new file mode 100755 index 00000000..88becfa8 --- /dev/null +++ b/src/scratch/blocks/Advanced/List/lists_getIndex.js @@ -0,0 +1,169 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.lists_getIndex = { + init() { + this.MODE_OPTIONS = [ + [translate('get'), 'GET'], + [translate('get and remove'), 'GET_REMOVE'], + [translate('remove'), 'REMOVE'], + ]; + this.WHERE_OPTIONS = [ + ['#', 'FROM_START'], + [translate('# from end'), 'FROM_END'], + [translate('first'), 'FIRST'], + [translate('last'), 'LAST'], + [translate('random'), 'RANDOM'], + ]; + const modeMenu = new Blockly.FieldDropdown(this.MODE_OPTIONS, value => { + const isStatement = value === 'REMOVE'; + this.updateStatement(isStatement); + }); + + this.appendValueInput('VALUE') + .setCheck('Array') + .appendField(translate('in list')); + this.appendDummyInput().appendField(modeMenu, 'MODE'); + this.appendDummyInput('AT'); + // eslint-disable-next-line no-underscore-dangle + this.setColourFromRawValues_( + Blockly.Colours.Binary.colour, + Blockly.Colours.Binary.colourSecondary, + Blockly.Colours.Binary.colourTertiary + ); + this.setOutput(true, null); + + this.updateAt(true); + }, + mutationToDom() { + const container = document.createElement('mutation'); + const isStatement = !this.outputConnection; + const isAt = this.getInput('AT').type === Blockly.INPUT_VALUE; + + container.setAttribute('statement', isStatement); + container.setAttribute('at', isAt); + + return container; + }, + domToMutation(xmlElement) { + const isStatement = xmlElement.getAttribute('statement') === 'true'; + this.updateStatement(isStatement); + + const isAt = xmlElement.getAttribute('at') !== 'false'; + this.updateAt(isAt); + }, + updateStatement(newStatement) { + const oldStatement = !this.outputConnection; + + if (newStatement !== oldStatement) { + this.unplug(true, true); + + this.setOutput(!newStatement); + this.setPreviousStatement(newStatement); + this.setNextStatement(newStatement); + + this.initSvg(); + this.render(false); + } + }, + updateAt(isAt) { + this.removeInput('AT', true); + + if (isAt) { + this.appendValueInput('AT').setCheck('Number'); + } else { + this.appendDummyInput('AT'); + } + + const menu = new Blockly.FieldDropdown(this.WHERE_OPTIONS, value => { + const newAt = ['FROM_START', 'FROM_END'].includes(value); + if (newAt !== isAt) { + this.updateAt(newAt); + this.setFieldValue(value, 'WHERE'); + return null; + } + return undefined; + }); + + this.getInput('AT').appendField(menu, 'WHERE'); + + this.initSvg(); + this.render(false); + }, +}; + +Blockly.JavaScript.lists_getIndex = block => { + const mode = block.getFieldValue('MODE') || 'GET'; + const where = block.getFieldValue('WHERE') || 'FIRST'; + const listOrder = where === 'RANDOM' ? Blockly.JavaScript.ORDER_COMMA : Blockly.JavaScript.ORDER_MEMBER; + const list = Blockly.JavaScript.valueToCode(block, 'VALUE', listOrder) || '[]'; + + let code, + order; + + if (where === 'FIRST') { + if (mode === 'GET') { + code = `${list}[0]`; + order = Blockly.JavaScript.ORDER_MEMBER; + } else if (mode === 'GET_REMOVE') { + code = `${list}.shift()`; + order = Blockly.JavaScript.ORDER_MEMBER; + } else if (mode === 'REMOVE') { + return `${list}.shift();\n`; + } + } else if (where === 'LAST') { + if (mode === 'GET') { + code = `${list}.slice(-1)[0]`; + order = Blockly.JavaScript.ORDER_MEMBER; + } else if (mode === 'GET_REMOVE') { + code = `${list}.pop()`; + order = Blockly.JavaScript.ORDER_MEMBER; + } else if (mode === 'REMOVE') { + return `${list}.pop();\n`; + } + } else if (where === 'FROM_START') { + const at = Blockly.JavaScript.getAdjusted(block, 'AT'); + if (mode === 'GET') { + code = `${list}[${at}]`; + order = Blockly.JavaScript.ORDER_MEMBER; + } else if (mode === 'GET_REMOVE') { + code = `${list}.splice(${at}, 1)[0]`; + order = Blockly.JavaScript.ORDER_FUNCTION_CALL; + } else if (mode === 'REMOVE') { + return `${list}.splice(${at}, 1);\n`; + } + } else if (where === 'FROM_END') { + const at = Blockly.JavaScript.getAdjusted(block, 'AT', 1, true); + if (mode === 'GET') { + code = `${list}.slice(${at})[0]`; + order = Blockly.JavaScript.ORDER_FUNCTION_CALL; + } else if (mode === 'GET_REMOVE') { + code = `${list}.splice(${at}, 1)[0]`; + order = Blockly.JavaScript.ORDER_FUNCTION_CALL; + } else if (mode === 'REMOVE') { + return `${list}.splice(${at}, 1);\n`; + } + } else if (where === 'RANDOM') { + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.provideFunction_('listsGetRandomItem', [ + // eslint-disable-next-line no-underscore-dangle + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(list, remove) { + var x = Math.floor(Math.random() * list.length); + if (remove) { + return list.splice(x, 1)[0]; + } else { + return list[x]; + } + }`, + ]); + + code = `${functionName}(${list}, ${mode !== 'GET'})`; + + if (mode === 'GET' || mode === 'GET_REMOVE') { + order = Blockly.JavaScript.ORDER_FUNCTION_CALL; + } else if (mode === 'REMOVE') { + return `${code};\n`; + } + } + + return [code, order]; +}; diff --git a/src/scratch/blocks/Advanced/List/lists_getSublist.js b/src/scratch/blocks/Advanced/List/lists_getSublist.js new file mode 100755 index 00000000..aeda49a8 --- /dev/null +++ b/src/scratch/blocks/Advanced/List/lists_getSublist.js @@ -0,0 +1,128 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.lists_getSublist = { + init() { + this.WHERE_OPTIONS_1 = [ + [translate('get sub-list from #'), 'FROM_START'], + [translate('get sub-list from # from end'), 'FROM_END'], + [translate('get sub-list from first'), 'FIRST'], + ]; + this.WHERE_OPTIONS_2 = [ + [translate('#'), 'FROM_START'], + [translate('# from end'), 'FROM_END'], + [translate('last'), 'LAST'], + ]; + + this.appendValueInput('LIST').appendField(translate('in list')); + this.appendDummyInput('AT1'); + this.appendDummyInput('AT2'); + + // eslint-disable-next-line no-underscore-dangle + this.setColourFromRawValues_( + Blockly.Colours.Binary.colour, + Blockly.Colours.Binary.colourSecondary, + Blockly.Colours.Binary.colourTertiary + ); + this.setOutput(true, null); + this.setOutputShape(Blockly.OUTPUT_SHAPE_ROUND); + + this.updateAt(1, true); + this.updateAt(2, true); + }, + mutationToDom() { + const container = document.createElement('mutation'); + const isAt1 = this.getInput('AT1').type === Blockly.INPUT_VALUE; + const isAt2 = this.getInput('AT2').type === Blockly.INPUT_VALUE; + + container.setAttribute('at1', isAt1); + container.setAttribute('at2', isAt2); + + return container; + }, + domToMutation(xmlElement) { + const isAt1 = xmlElement.getAttribute('at1') === 'true'; + const isAt2 = xmlElement.getAttribute('at2') === 'true'; + this.updateAt(1, isAt1); + this.updateAt(2, isAt2); + }, + updateAt(n, isAt) { + this.removeInput(`AT${n}`); + + if (isAt) { + this.appendValueInput(`AT${n}`).setCheck('Number'); + } else { + this.appendDummyInput(`AT${n}`); + } + + const menu = new Blockly.FieldDropdown(this[`WHERE_OPTIONS_${n}`], value => { + const newAt = ['FROM_START', 'FROM_END'].includes(value); + if (newAt !== isAt) { + this.updateAt(n, newAt); + this.setFieldValue(value, `WHERE${n}`); + return null; + } + return undefined; + }); + + this.getInput(`AT${n}`).appendField(menu, `WHERE${n}`); + + if (n === 1) { + this.moveInputBefore('AT1', 'AT2'); + } + + this.initSvg(); + this.render(false); + }, +}; + +Blockly.JavaScript.lists_getSublist = block => { + const list = Blockly.JavaScript.valueToCode(block, 'LIST', Blockly.JavaScript.ORDER_MEMBER) || '[]'; + const where1 = block.getFieldValue('WHERE1'); + const where2 = block.getFieldValue('WHERE2'); + + let at1, + at2, + code; + + if (list.match(/^\w+$/)) { + if (where1 === 'FROM_START') { + at1 = Blockly.JavaScript.getAdjusted(block, 'AT1'); + } else if (where1 === 'FROM_END') { + at1 = Blockly.JavaScript.getAdjusted(block, 'AT1', 1, false, Blockly.JavaScript.ORDER_SUBTRACTION); + at1 = `${list}.length - ${at1}`; + } + if (where2 === 'FROM_START') { + at2 = Blockly.JavaScript.getAdjusted(block, 'AT2', 1); + } else if (where2 === 'FROM_END') { + at2 = Blockly.JavaScript.getAdjusted(block, 'AT2', 0, false, Blockly.JavaScript.ORDER_SUBTRACTION); + at2 = `${list}.length - ${at2}`; + } + + code = `${list}.slice(${at1}, ${at2})`; + } else { + at1 = Blockly.JavaScript.getAdjusted(block, 'AT1'); + at2 = Blockly.JavaScript.getAdjusted(block, 'AT2'); + const wherePascalCase = { + FROM_START: 'FromStart', + FROM_END : 'FromEnd', + }; + const getIndex = (listName, where, at) => (where === 'FROM_END' ? `${listName}.length - 1 - ${at}` : at); + + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.provideFunction_( + `subsequence${wherePascalCase[where1]}${wherePascalCase[where2]}`, + [ + // eslint-disable-next-line no-underscore-dangle + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(sequence, at1, at2) { + var start = ${getIndex('sequence', where1, 'at1')}; + var end = ${getIndex('sequence', where2, 'at2')}; + return sequence.slice(start, end); + }`, + ] + ); + + code = `${functionName}(${list}, ${at1}, ${at2})`; + } + + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Advanced/List/lists_indexOf.js b/src/scratch/blocks/Advanced/List/lists_indexOf.js new file mode 100755 index 00000000..5ae99e1f --- /dev/null +++ b/src/scratch/blocks/Advanced/List/lists_indexOf.js @@ -0,0 +1,43 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.lists_indexOf = { + init() { + this.jsonInit({ + message0: translate('in list %1 find %2 occurence of item %3'), + args0 : [ + { + type: 'input_value', + name: 'VALUE', + }, + { + type : 'field_dropdown', + name : 'END', + options: [[translate('first'), 'FIRST'], [translate('last'), 'LAST']], + }, + { + type: 'input_value', + name: 'FIND', + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.lists_indexOf = block => { + const operator = block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; + const item = Blockly.JavaScript.valueToCode(block, 'FIND', Blockly.JavaScript.ORDER_NONE) || '\'\''; + const list = Blockly.JavaScript.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_MEMBER) || '\'\''; + + const code = `${list}.${operator}(${item})`; + + if (block.workspace.options.oneBasedIndex) { + return [`${code} + 1`, Blockly.JavaScript.ORDER_ADDITION]; + } + + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Advanced/List/lists_isEmpty.js b/src/scratch/blocks/Advanced/List/lists_isEmpty.js new file mode 100755 index 00000000..b22d2d9e --- /dev/null +++ b/src/scratch/blocks/Advanced/List/lists_isEmpty.js @@ -0,0 +1,29 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.lists_isEmpty = { + init() { + this.jsonInit({ + message0: translate('list %1 is empty'), + args0 : [ + { + type : 'input_value', + name : 'VALUE', + check: ['Array'], + }, + ], + output : 'Boolean', + outputShape : Blockly.OUTPUT_SHAPE_HEXAGONAL, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.lists_isEmpty = block => { + const list = Blockly.JavaScript.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_MEMBER) || '[]'; + const isVariable = block.workspace.getAllVariables().findIndex(variable => variable.name === list) !== -1; + + const code = isVariable ? `!${list} || !${list}.length` : `!${list}.length`; + return [code, Blockly.JavaScript.ORDER_LOGICAL_NOT]; +}; diff --git a/src/scratch/blocks/Advanced/List/lists_length.js b/src/scratch/blocks/Advanced/List/lists_length.js new file mode 100755 index 00000000..9e73be6e --- /dev/null +++ b/src/scratch/blocks/Advanced/List/lists_length.js @@ -0,0 +1,27 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.lists_length = { + init() { + this.jsonInit({ + message0: translate('length of %1'), + args0 : [ + { + type: 'input_value', + name: 'VALUE', + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.lists_length = block => { + const list = Blockly.JavaScript.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_MEMBER) || '[]'; + + const code = `${list}.length`; + return [code, Blockly.JavaScript.ORDER_MEMBER]; +}; diff --git a/src/scratch/blocks/Advanced/List/lists_repeat.js b/src/scratch/blocks/Advanced/List/lists_repeat.js new file mode 100755 index 00000000..af19acff --- /dev/null +++ b/src/scratch/blocks/Advanced/List/lists_repeat.js @@ -0,0 +1,54 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.lists_repeat = { + init() { + this.jsonInit({ + message0: translate('set %1 to item %2 repeated %3 times'), + args0 : [ + { + type : 'field_variable', + name : 'VARIABLE', + variable: translate('list'), + }, + { + type: 'input_value', + name: 'ITEM', + }, + { + type: 'input_value', + name: 'NUM', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, +}; + +Blockly.JavaScript.lists_repeat = block => { + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('VARIABLE'), + Blockly.Variables.NAME_TYPE + ); + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.provideFunction_('listsRepeat', [ + // eslint-disable-next-line no-underscore-dangle + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(value, n) { + var array = []; + for (var i = 0; i < n; i++) { + array[i] = value; + } + return array; + }`, + ]); + + const element = Blockly.JavaScript.valueToCode(block, 'ITEM', Blockly.JavaScript.ORDER_COMMA) || 'null'; + const repeatCount = Blockly.JavaScript.valueToCode(block, 'NUM', Blockly.JavaScript.ORDER_COMMA) || '0'; + + const code = `${varName} = ${functionName}(${element}, ${repeatCount});\n`; + return code; +}; diff --git a/src/scratch/blocks/Advanced/List/lists_setIndex.js b/src/scratch/blocks/Advanced/List/lists_setIndex.js new file mode 100755 index 00000000..79227619 --- /dev/null +++ b/src/scratch/blocks/Advanced/List/lists_setIndex.js @@ -0,0 +1,135 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.lists_setIndex = { + init() { + this.MODE_OPTIONS = [[translate('set'), 'SET'], [translate('insert at'), 'INSERT']]; + this.WHERE_OPTIONS = [ + [translate('#'), 'FROM_START'], + [translate('# from end'), 'FROM_END'], + [translate('first'), 'FIRST'], + [translate('last'), 'LAST'], + [translate('random'), 'RANDOM'], + ]; + + this.appendValueInput('LIST') + .setCheck('Array') + .appendField(translate('in list')); + this.appendDummyInput().appendField(new Blockly.FieldDropdown(this.MODE_OPTIONS), 'MODE'); + this.appendDummyInput('AT'); + this.appendValueInput('TO').appendField(translate('as')); + + // eslint-disable-next-line no-underscore-dangle + this.setColourFromRawValues_( + Blockly.Colours.Binary.colour, + Blockly.Colours.Binary.colourSecondary, + Blockly.Colours.Binary.colourTertiary + ); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + + this.updateAt(true); + }, + mutationToDom() { + const container = document.createElement('mutation'); + const isAt = this.getInput('AT').type === Blockly.INPUT_VALUE; + + container.setAttribute('at', isAt); + return container; + }, + domToMutation(xmlElement) { + const isAt = xmlElement.getAttribute('at') !== 'false'; + this.updateAt(isAt); + }, + updateAt(isAt) { + this.removeInput('AT', true); + + if (isAt) { + this.appendValueInput('AT').setCheck('Number'); + } else { + this.appendDummyInput('AT'); + } + + const menu = new Blockly.FieldDropdown(this.WHERE_OPTIONS, value => { + const newAt = ['FROM_START', 'FROM_END'].includes(value); + if (newAt !== isAt) { + this.updateAt(newAt); + this.setFieldValue(value, 'WHERE'); + return null; + } + return undefined; + }); + + this.moveInputBefore('AT', 'TO'); + this.getInput('AT').appendField(menu, 'WHERE'); + this.initSvg(); + this.render(false); + }, +}; + +Blockly.JavaScript.lists_setIndex = block => { + const mode = block.getFieldValue('MODE') || 'SET'; + const where = block.getFieldValue('WHERE') || 'FIRST'; + const value = Blockly.JavaScript.valueToCode(block, 'TO', Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null'; + + let list = Blockly.JavaScript.valueToCode(block, 'LIST', Blockly.JavaScript.ORDER_MEMBER) || '[]'; + + const cacheList = () => { + if (list.match(/^\w+$/)) { + return ''; + } + + // eslint-disable-next-line no-underscore-dangle + const listVar = Blockly.JavaScript.variableDB_.getDistinctName('tmpList', Blockly.Variables.NAME_TYPE); + const code = `var ${listVar} = ${list};\n`; + + list = listVar; + return code; + }; + + let code; + + if (where === 'FIRST') { + if (mode === 'SET') { + code = `${list}[0] = ${value};\n`; + } else if (mode === 'INSERT') { + code = `${list}.unshift(${value});\n`; + } + } else if (where === 'LAST') { + if (mode === 'SET') { + code = cacheList(); + code += `${list}[${list}.length - 1] = ${value};\n`; + } else if (mode === 'INSERT') { + code = `${list}.push(${value});\n`; + } + } else if (where === 'FROM_START') { + const at = Blockly.JavaScript.getAdjusted(block, 'AT'); + if (mode === 'SET') { + code = `${list}[${at}] = ${value};\n`; + } else if (mode === 'INSERT') { + code = `${list}.splice(${at}, 0, ${value});\n`; + } + } else if (where === 'FROM_END') { + const at = Blockly.JavaScript.getAdjusted(block, 'AT', 1, false, Blockly.JavaScript.ORDER_SUBTRACTION); + code = cacheList(); + if (mode === 'SET') { + code = `${list}[${list}.length - ${at}] = ${value};\n`; + } else if (mode === 'INSERT') { + code = `${list}.splice(${list}.length - ${at}, 0, ${value});\n`; + } + } else if (where === 'RANDOM') { + code = cacheList(); + + // eslint-disable-next-line no-underscore-dangle + const xVar = Blockly.JavaScript.variableDB_.getDistinctName('tmpX', Blockly.Variables.NAME_TYPE); + + code += `var ${xVar} = Math.floor(Math.random() * ${list}.length);\n`; + + if (mode === 'SET') { + code += `${list}[${xVar}] = ${value};\n`; + } else if (mode === 'INSERT') { + code += `${list}.splice(${xVar}, 0, ${value});\n`; + } + } + + return code; +}; diff --git a/src/scratch/blocks/Advanced/List/lists_sort.js b/src/scratch/blocks/Advanced/List/lists_sort.js new file mode 100755 index 00000000..197855c2 --- /dev/null +++ b/src/scratch/blocks/Advanced/List/lists_sort.js @@ -0,0 +1,57 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.lists_sort = { + init() { + this.jsonInit({ + message0: translate('sort %1 %2 %3'), + args0 : [ + { + type : 'field_dropdown', + name : 'TYPE', + options: [[translate('numeric'), 'NUMERIC'], [translate('alphabetic'), 'TEXT']], + }, + { + type : 'field_dropdown', + name : 'DIRECTION', + options: [[translate('ascending'), '1'], [translate('descending'), '-1']], + }, + { + type: 'input_value', + name: 'LIST', + }, + ], + output : 'Array', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.lists_sort = block => { + const list = Blockly.JavaScript.valueToCode(block, 'LIST', Blockly.JavaScript.ORDER_FUNCTION_CALL) || '[]'; + const direction = block.getFieldValue('DIRECTION') === '1' ? 1 : -1; + const type = block.getFieldValue('TYPE'); + // eslint-disable-next-line no-underscore-dangle + const getCompareFunctionName = Blockly.JavaScript.provideFunction_('listsGetSortCompare', [ + // eslint-disable-next-line no-underscore-dangle + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(type, direction) { + var compareFuncs = { + NUMERIC: function(a, b) { + return parseFloat(a) - parseFloat(b); + }, + TEXT: function(a, b) { + return a.toString().toLowerCase() > b.toString().toLowerCase() ? 1 : -1; + } + }; + + return function(a, b) { + return compareFuncs[type](a, b) * direction; + } + }`, + ]); + + const code = `${list}.slice(0).sort(${getCompareFunctionName}("${type}", ${direction}))`; + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Advanced/List/lists_split.js b/src/scratch/blocks/Advanced/List/lists_split.js new file mode 100755 index 00000000..6c193107 --- /dev/null +++ b/src/scratch/blocks/Advanced/List/lists_split.js @@ -0,0 +1,77 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.lists_split = { + init() { + const dropdown = new Blockly.FieldDropdown( + [[translate('make list from text'), 'SPLIT'], [translate('make text from list'), 'JOIN']], + newMode => this.updateType(newMode) + ); + + this.appendValueInput('INPUT') + .setCheck('String') + .appendField(dropdown, 'MODE'); + this.appendValueInput('DELIM') + .setCheck('String') + .appendField('', 'SPACE1') + .appendField(translate('with delimiter'), 'DELIM_LABEL'); + this.appendDummyInput().appendField('', 'SPACE2'); + + this.setOutput(true, 'Array'); + this.setOutputShape(Blockly.OUTPUT_SHAPE_ROUND); + + // eslint-disable-next-line no-underscore-dangle + this.setColourFromRawValues_( + Blockly.Colours.Binary.colour, + Blockly.Colours.Binary.colourSecondary, + Blockly.Colours.Binary.colourTertiary + ); + }, + mutationToDom() { + const container = document.createElement('mutation'); + container.setAttribute('mode', this.getFieldValue('MODE')); + return container; + }, + domToMutation(xmlElement) { + this.updateType(xmlElement.getAttribute('mode')); + }, + updateType(newMode) { + const delimInput = this.getInput('DELIM'); + const spaceField = this.getField('SPACE1'); + + if (newMode === 'SPLIT') { + this.outputConnection.setCheck('Array'); + this.getInput('INPUT').setCheck('String'); + + // Create extra spacing for OUTPUT_SHAPE_SQUARE (i.e. string shapes) + if (!spaceField) { + delimInput.insertFieldAt(0, '', 'SPACE1'); + } + } else { + this.outputConnection.setCheck('String'); + this.getInput('INPUT').setCheck(null); + + if (spaceField) { + delimInput.removeField('SPACE1'); + } + } + + this.initSvg(); + this.render(false); + }, +}; + +Blockly.JavaScript.lists_split = block => { + const input = Blockly.JavaScript.valueToCode(block, 'INPUT', Blockly.JavaScript.ORDER_MEMBER); + const delimiter = Blockly.JavaScript.valueToCode(block, 'DELIM', Blockly.JavaScript.ORDER_NONE) || '\'\''; + const mode = block.getFieldValue('MODE'); + + let code; + + if (mode === 'SPLIT') { + code = `${input || '\'\''}.split(${delimiter})`; + } else if (mode === 'JOIN') { + code = `${input || '[]'}.join(${delimiter})`; + } + + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Advanced/List/lists_statement.js b/src/scratch/blocks/Advanced/List/lists_statement.js new file mode 100755 index 00000000..468dd6de --- /dev/null +++ b/src/scratch/blocks/Advanced/List/lists_statement.js @@ -0,0 +1,64 @@ +import { minusIconDark } from '../../images'; + +Blockly.Blocks.lists_statement = { + init() { + this.requiredParentId = ''; + + this.jsonInit({ + message0: '%1', + args0 : [ + { + type: 'input_value', + name: 'VALUE', + }, + ], + colour : Blockly.Colours.BinaryLessGray.colour, + colourSecondary : Blockly.Colours.BinaryLessGray.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessGray.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + // Render a ➖-icon for removing self + const fieldImage = new Blockly.FieldImage(minusIconDark, 25, 25, '', () => this.onIconClick()); + this.appendDummyInput('REMOVE_ICON').appendField(fieldImage); + }, + onIconClick() { + if (!this.workspace || this.isInFlyout) { + return; + } + + this.unplug(true); + this.dispose(); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + const surroundParent = this.getSurroundParent(); + if (event.type === Blockly.Events.END_DRAG) { + if (!this.requiredParentId && surroundParent.type === 'lists_create_with') { + this.requiredParentId = surroundParent.id; + } else if (!surroundParent || surroundParent.id !== this.requiredParentId) { + Blockly.Events.disable(); + this.unplug(false); + + const parentBlock = this.workspace.getAllBlocks().find(block => block.id === this.requiredParentId); + + if (parentBlock) { + const parentConnection = parentBlock.getLastConnectionInStatement('STACK'); + parentConnection.connect(this.previousConnection); + } else { + this.dispose(); + } + Blockly.Events.enable(); + } + } + }, +}; + +Blockly.JavaScript.lists_statement = block => { + const code = Blockly.JavaScript.valueToCode(block, 'VALUE') || 'null'; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Advanced/Loops/controls_flow_statements.js b/src/scratch/blocks/Advanced/Loops/controls_flow_statements.js new file mode 100755 index 00000000..21ff0c09 --- /dev/null +++ b/src/scratch/blocks/Advanced/Loops/controls_flow_statements.js @@ -0,0 +1,29 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.controls_flow_statements = { + init() { + this.jsonInit({ + message0: translate('%1 of loop'), + args0 : [ + { + type : 'field_dropdown', + name : 'FLOW', + options: [ + [translate('break out'), 'BREAK'], + [translate('continue with next iteration'), 'CONTINUE'], + ], + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, +}; + +Blockly.JavaScript.controls_flow_statements = block => { + const keyword = block.getFieldValue('FLOW') === 'BREAK' ? 'break' : 'continue'; + return `${keyword};\n`; +}; diff --git a/src/scratch/blocks/Advanced/Loops/controls_for.js b/src/scratch/blocks/Advanced/Loops/controls_for.js new file mode 100755 index 00000000..9b9e30c8 --- /dev/null +++ b/src/scratch/blocks/Advanced/Loops/controls_for.js @@ -0,0 +1,109 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.controls_for = { + init() { + this.jsonInit({ + message0: translate('count with %1 from %2 to %3 by %4'), + args0 : [ + { + type : 'field_variable', + name : 'VAR', + variable: null, + }, + { + type : 'input_value', + name : 'FROM', + check: 'Number', + }, + { + type : 'input_value', + name : 'TO', + check: 'Number', + }, + { + type : 'input_value', + name : 'BY', + check: 'Number', + }, + ], + message1: translate('do %1'), + args1 : [ + { + type: 'input_statement', + name: 'DO', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, +}; + +Blockly.JavaScript.controls_for = block => { + // eslint-disable-next-line no-underscore-dangle + const variable0 = Blockly.JavaScript.variableDB_.getName(block.getFieldValue('VAR'), Blockly.Variables.NAME_TYPE); + const argument0 = Blockly.JavaScript.valueToCode(block, 'FROM', Blockly.JavaScript.ORDER_ASSIGNMENT) || '0'; + const argument1 = Blockly.JavaScript.valueToCode(block, 'TO', Blockly.JavaScript.ORDER_ASSIGNMENT) || '0'; + const increment = Blockly.JavaScript.valueToCode(block, 'BY', Blockly.JavaScript.ORDER_ASSIGNMENT) || '1'; + + let branch = Blockly.JavaScript.statementToCode(block, 'DO'); + branch = Blockly.JavaScript.addLoopTrap(branch, block.id); + + let code = ''; + + if (Blockly.isNumber(argument0) && Blockly.isNumber(argument1) && Blockly.isNumber(increment)) { + const up = parseFloat(argument0) <= parseFloat(argument1); + const operator = up ? '<=' : '>='; + const step = Math.abs(parseFloat(increment)); + + const assignment = `${variable0} = ${argument0}`; + const condition = `${variable0} ${operator} ${argument1}`; + const statement = `${variable0} ${up ? '+=' : '-='} ${step}`; + + code = ` + for (${assignment}; ${condition}; ${statement}) { + ${branch} + }\n`; + } else { + // Cache non-trivial values to variables to prevent repeated look-ups. + let startVar = argument0; + if (!argument0.match(/^\w+$/) && !Blockly.isNumber(argument0)) { + // eslint-disable-next-line no-underscore-dangle + startVar = Blockly.JavaScript.variableDB_.getDistinctName( + `${variable0}_start`, + Blockly.Variables.NAME_TYPE + ); + code = `var ${startVar} = ${argument0};\n`; + } + + let endVar = argument1; + if (!argument1.match(/^\w+$/) && !Blockly.isNumber(argument1)) { + // eslint-disable-next-line no-underscore-dangle + endVar = Blockly.JavaScript.variableDB_.getDistinctName(`${variable0}_end`, Blockly.Variables.NAME_TYPE); + code += `var ${endVar} = ${argument1};\n`; + } + + // Determine loop direction at start, in case one of the bounds changes during loop execution. + // eslint-disable-next-line no-underscore-dangle + const incVar = Blockly.JavaScript.variableDB_.getDistinctName(`${variable0}_inc`, Blockly.Variables.NAME_TYPE); + const incVal = Blockly.isNumber(increment) ? Math.abs(increment) : `Math.abs(${increment})`; + + code += ` + var ${incVar} = ${incVal}; + if (${startVar} > ${endVar}) { + ${incVar} = -${incVar}; + } + for ( + ${variable0} = ${startVar}; + ${incVar} >= 0 ? ${variable0} <= ${endVar} : ${variable0} >= ${endVar}; + ${variable0} += ${incVar} + ) { + ${branch}; + }\n`; + } + + return code; +}; diff --git a/src/scratch/blocks/Advanced/Loops/controls_forEach.js b/src/scratch/blocks/Advanced/Loops/controls_forEach.js new file mode 100755 index 00000000..902f51ed --- /dev/null +++ b/src/scratch/blocks/Advanced/Loops/controls_forEach.js @@ -0,0 +1,63 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.controls_forEach = { + init() { + this.jsonInit({ + message0: translate('for each item %1 in list %2'), + args0 : [ + { + type : 'field_variable', + name : 'VAR', + variable: null, + }, + { + type : 'input_value', + name : 'LIST', + check: 'Array', + }, + ], + message1: translate('do %1'), + args1 : [ + { + type: 'input_statement', + name: 'DO', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, +}; + +Blockly.JavaScript.controls_forEach = block => { + // eslint-disable-next-line no-underscore-dangle + const variable0 = Blockly.JavaScript.variableDB_.getName(block.getFieldValue('VAR'), Blockly.Variables.NAME_TYPE); + const argument0 = Blockly.JavaScript.valueToCode(block, 'LIST', Blockly.JavaScript.ORDER_ASSIGNMENT) || '[]'; + + let branch = Blockly.JavaScript.statementToCode(block, 'DO'); + branch = Blockly.JavaScript.addLoopTrap(branch, block.id); + + let code = ''; + + // Cache non-trivial values to variables to prevent repeated look-ups. + let listVar = argument0; + if (!argument0.match(/^\w+$/)) { + // eslint-disable-next-line no-underscore-dangle + listVar = Blockly.JavaScript.variableDB_.getDistinctName(`${variable0}_list`, Blockly.Variables.NAME_TYPE); + code = `var ${listVar} = ${argument0};\n`; + } + + // eslint-disable-next-line no-underscore-dangle + const indexVar = Blockly.JavaScript.variableDB_.getDistinctName(`${variable0}_list`, Blockly.Variables.NAME_TYPE); + + code += ` + for (var ${indexVar} in ${listVar}) { + ${variable0} = ${listVar}[${indexVar}]; + ${branch} + }\n`; + + return code; +}; diff --git a/src/scratch/blocks/Advanced/Loops/controls_repeat.js b/src/scratch/blocks/Advanced/Loops/controls_repeat.js new file mode 100755 index 00000000..ee2dee68 --- /dev/null +++ b/src/scratch/blocks/Advanced/Loops/controls_repeat.js @@ -0,0 +1,33 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.controls_repeat = { + init() { + this.jsonInit({ + type : 'controls_repeat', + message0: translate('repeat %1 times'), + args0 : [ + { + type : 'field_number', + name : 'TIMES', + value : 10, + min : 0, + precision: 1, + }, + ], + message1: translate('do %1'), + args1 : [ + { + type: 'input_statement', + name: 'DO', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, +}; + +Blockly.JavaScript.controls_repeat = Blockly.JavaScript.controls_repeat_ext; diff --git a/src/scratch/blocks/Advanced/Loops/controls_repeat_ext.js b/src/scratch/blocks/Advanced/Loops/controls_repeat_ext.js new file mode 100755 index 00000000..91917016 --- /dev/null +++ b/src/scratch/blocks/Advanced/Loops/controls_repeat_ext.js @@ -0,0 +1,56 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.controls_repeat_ext = { + init() { + this.jsonInit({ + message0: translate('repeat %1 times'), + args0 : [ + { + type : 'input_value', + name : 'TIMES', + check: 'Number', + }, + ], + message1: translate('do %1'), + args1 : [ + { + type: 'input_statement', + name: 'DO', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, +}; + +Blockly.JavaScript.controls_repeat_ext = block => { + let repeats; + if (block.getField('TIMES')) { + repeats = String(Number(block.getFieldValue('TIMES'))); + } else { + repeats = Blockly.JavaScript.valueToCode(block, 'TIMES') || '0'; + } + + const branch = Blockly.JavaScript.statementToCode(block, 'DO'); + let code = ''; + + // eslint-disable-next-line no-underscore-dangle + const loopVar = Blockly.JavaScript.variableDB_.getDistinctName('count', Blockly.Variables.NAME_TYPE); + let endVar = repeats; + + if (!repeats.match(/^\w+$/) && !Blockly.isNumber(repeats)) { + // eslint-disable-next-line no-underscore-dangle + endVar = Blockly.JavaScript.variableDB_.getDistinctName('repeat_end', Blockly.Variables.NAME_TYPE); + code += `var ${endVar} = ${repeats};\n`; + } + + code += ` + for (var ${loopVar} = 0; ${loopVar} < ${endVar}; ${loopVar}++) { + ${branch} + }\n`; + return code; +}; diff --git a/src/scratch/blocks/Advanced/Loops/controls_whileUntil.js b/src/scratch/blocks/Advanced/Loops/controls_whileUntil.js new file mode 100755 index 00000000..71b33141 --- /dev/null +++ b/src/scratch/blocks/Advanced/Loops/controls_whileUntil.js @@ -0,0 +1,63 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.controls_whileUntil = { + init() { + this.jsonInit({ + message0: translate('repeat %1 %2'), + args0 : [ + { + type : 'field_dropdown', + name : 'MODE', + options: [[translate('while'), 'WHILE'], [translate('until'), 'UNTIL']], + }, + { + type : 'input_value', + name : 'BOOL', + check: 'Boolean', + }, + ], + message1: translate('do %1'), + args1 : [ + { + type: 'input_statement', + name: 'DO', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, +}; + +Blockly.JavaScript.controls_whileUntil = block => { + const branch = Blockly.JavaScript.statementToCode(block, 'DO'); + const until = block.getFieldValue('MODE') === 'UNTIL'; + const order = until ? Blockly.JavaScript.ORDER_LOGICAL_NOT : Blockly.JavaScript.ORDER_NONE; + let argument0 = Blockly.JavaScript.valueToCode(block, 'BOOL', order) || 'false'; + + if (until) { + argument0 = `!${argument0}`; + } + + // eslint-disable-next-line no-underscore-dangle + const maxLoopVar = Blockly.JavaScript.variableDB_.getDistinctName('maxLoops', Blockly.Variables.NAME_TYPE); + // eslint-disable-next-line no-underscore-dangle + const currentLoopVar = Blockly.JavaScript.variableDB_.getDistinctName('currentLoop', Blockly.Variables.NAME_TYPE); + + return ` + var ${maxLoopVar} = 10000; + var ${currentLoopVar} = 0; + + while (${argument0}) { + if (${currentLoopVar} > ${maxLoopVar}) { + throw new Error("${translate('Infinite loop detected')}"); + } else { + ${currentLoopVar}++; + } + + ${branch} + }\n`; +}; diff --git a/src/scratch/blocks/Advanced/Loops/index.js b/src/scratch/blocks/Advanced/Loops/index.js new file mode 100755 index 00000000..4510afa8 --- /dev/null +++ b/src/scratch/blocks/Advanced/Loops/index.js @@ -0,0 +1,6 @@ +import './controls_repeat_ext'; +import './controls_repeat'; +import './controls_whileUntil'; +import './controls_for'; +import './controls_forEach'; +import './controls_flow_statements'; diff --git a/src/scratch/blocks/Advanced/Variable/index.js b/src/scratch/blocks/Advanced/Variable/index.js new file mode 100755 index 00000000..277b3ac1 --- /dev/null +++ b/src/scratch/blocks/Advanced/Variable/index.js @@ -0,0 +1,2 @@ +import './variables_get'; +import './variables_set'; diff --git a/src/scratch/blocks/Advanced/Variable/variables_get.js b/src/scratch/blocks/Advanced/Variable/variables_get.js new file mode 100755 index 00000000..de7c7b09 --- /dev/null +++ b/src/scratch/blocks/Advanced/Variable/variables_get.js @@ -0,0 +1,29 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.variables_get = { + init() { + this.jsonInit({ + type : 'variables_get', + message0: '%1', + args0 : [ + { + type : 'field_variable', + name : 'VAR', + variable: translate('item'), + }, + ], + output : null, + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : '', + }); + }, +}; + +Blockly.JavaScript.variables_get = block => { + // eslint-disable-next-line no-underscore-dangle + const code = Blockly.JavaScript.variableDB_.getName(block.getFieldValue('VAR'), Blockly.Variables.NAME_TYPE); + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Advanced/Variable/variables_set.js b/src/scratch/blocks/Advanced/Variable/variables_set.js new file mode 100755 index 00000000..ac8c05c3 --- /dev/null +++ b/src/scratch/blocks/Advanced/Variable/variables_set.js @@ -0,0 +1,36 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.variables_set = { + init() { + this.jsonInit({ + type : 'field_variable', + message0: translate('set %1 to %2'), + args0 : [ + { + type : 'field_variable', + name : 'VAR', + variable: translate('item'), + }, + { + type: 'input_value', + name: 'VALUE', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + tooltip : '', + }); + }, +}; + +Blockly.JavaScript.variables_set = block => { + const argument0 = Blockly.JavaScript.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_ASSIGNMENT) || '0'; + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName(block.getFieldValue('VAR'), Blockly.Variables.NAME_TYPE); + + const code = `${varName} = ${argument0};\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/After Purchase/after_purchase.js b/src/scratch/blocks/Binary/After Purchase/after_purchase.js new file mode 100755 index 00000000..a604df20 --- /dev/null +++ b/src/scratch/blocks/Binary/After Purchase/after_purchase.js @@ -0,0 +1,64 @@ +import { finishSign } from '../../images'; +import { setBlockTextColor } from '../../../utils'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.after_purchase = { + init() { + this.jsonInit({ + message0: translate('%1 (4) Get your trade result and trade again %2'), + message1: '%1', + args0 : [ + { + type : 'field_image', + src : finishSign, + width : 25, + height: 25, + alt : 'F', + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_statement', + name : 'AFTERPURCHASE_STACK', + check: 'TradeAgain', + }, + ], + colour : '#2a3052', + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate( + 'Get the previous trade information and result, then trade again (Runs on trade finish)' + ), + }); + }, + onchange(event) { + setBlockTextColor(this); + if (!this.workspace || this.isInFlyout) { + return; + } + + // Maintain single instance of this block + if (event.type === Blockly.Events.BLOCK_CREATE) { + if (event.ids && event.ids.includes(this.id)) { + this.workspace.getAllBlocks(true).forEach(block => { + if (block.type === this.type && block.id !== this.id) { + block.dispose(); + } + }); + } + } + }, +}; + +Blockly.JavaScript.after_purchase = block => { + const stack = Blockly.JavaScript.statementToCode(block, 'AFTERPURCHASE_STACK'); + const code = ` + BinaryBotPrivateAfterPurchase = function BinaryBotPrivateAfterPurchase() { + ${stack} + return false; + };`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/After Purchase/check_result.js b/src/scratch/blocks/Binary/After Purchase/check_result.js new file mode 100755 index 00000000..976a68ed --- /dev/null +++ b/src/scratch/blocks/Binary/After Purchase/check_result.js @@ -0,0 +1,45 @@ +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.contract_check_result = { + init() { + this.jsonInit({ + message0: translate('Result is %1'), + args0 : [ + { + type : 'field_dropdown', + name : 'CHECK_RESULT', + options: config.lists.CHECK_RESULT, + }, + ], + output : 'Boolean', + outputShape : Blockly.OUTPUT_SHAPE_HEXAGONAL, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('True if the result matches the selection'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + if (this.isDescendantOf('after_purchase')) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.contract_check_result = block => { + const checkWith = block.getFieldValue('CHECK_RESULT'); + + const code = `Bot.isResult('${checkWith}')`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/After Purchase/index.js b/src/scratch/blocks/Binary/After Purchase/index.js new file mode 100755 index 00000000..9d47e3db --- /dev/null +++ b/src/scratch/blocks/Binary/After Purchase/index.js @@ -0,0 +1,4 @@ +import './after_purchase'; +import './check_result'; +import './read_details'; +import './trade_again'; diff --git a/src/scratch/blocks/Binary/After Purchase/read_details.js b/src/scratch/blocks/Binary/After Purchase/read_details.js new file mode 100755 index 00000000..c131b86a --- /dev/null +++ b/src/scratch/blocks/Binary/After Purchase/read_details.js @@ -0,0 +1,44 @@ +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.read_details = { + init() { + this.jsonInit({ + message0: translate('Contract Detail: %1'), + args0 : [ + { + type : 'field_dropdown', + name : 'DETAIL_INDEX', + options: config.lists.DETAILS, + }, + ], + output : null, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Reads a selected option from contract details list'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + if (this.isDescendantOf('after_purchase')) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.read_details = block => { + const detailIndex = block.getFieldValue('DETAIL_INDEX'); + + const code = `Bot.readDetails(${detailIndex})`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/After Purchase/trade_again.js b/src/scratch/blocks/Binary/After Purchase/trade_again.js new file mode 100755 index 00000000..ff387d6c --- /dev/null +++ b/src/scratch/blocks/Binary/After Purchase/trade_again.js @@ -0,0 +1,34 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.trade_again = { + init() { + this.jsonInit({ + message0 : translate('Trade Again'), + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + tooltip : translate('Runs the trade block again'), + }); + + // Ensure one of this type per statement-stack + this.setNextStatement(false); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + if (this.isDescendantOf('after_purchase')) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.trade_again = () => 'return true;\n'; diff --git a/src/scratch/blocks/Binary/Before Purchase/ask_price.js b/src/scratch/blocks/Binary/Before Purchase/ask_price.js new file mode 100755 index 00000000..feab3985 --- /dev/null +++ b/src/scratch/blocks/Binary/Before Purchase/ask_price.js @@ -0,0 +1,45 @@ +import { getPurchaseChoices } from '../../../shared'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.ask_price = { + init() { + this.jsonInit({ + message0: translate('Ask Price %1'), + args0 : [ + { + type : 'field_dropdown', + name : 'PURCHASE_LIST', + options: getPurchaseChoices, + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Ask Price for selected proposal'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + if (this.isDescendantOf('before_purchase')) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.ask_price = block => { + const purchaseList = block.getFieldValue('PURCHASE_LIST'); + + const code = `Bot.getAskPrice('${purchaseList}')`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Before Purchase/before_purchase.js b/src/scratch/blocks/Binary/Before Purchase/before_purchase.js new file mode 100755 index 00000000..35243125 --- /dev/null +++ b/src/scratch/blocks/Binary/Before Purchase/before_purchase.js @@ -0,0 +1,61 @@ +import { purchase } from '../../images'; +import { setBlockTextColor } from '../../../utils'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.before_purchase = { + init() { + this.jsonInit({ + message0: translate('%1 (2) Watch and purchase your contract %2'), + message1: '%1', + args0 : [ + { + type : 'field_image', + src : purchase, + width : 25, + height: 25, + alt : 'P', + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_statement', + name : 'BEFOREPURCHASE_STACK', + check: 'Purchase', + }, + ], + colour : '#2a3052', + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Watch the tick stream and purchase the desired contract (Runs on tick update)'), + }); + }, + onchange(event) { + setBlockTextColor(this); + if (!this.workspace || this.isInFlyout) { + return; + } + + // Maintain single instance of this block + if (event.type === Blockly.Events.BLOCK_CREATE) { + if (event.ids && event.ids.includes(this.id)) { + this.workspace.getAllBlocks(true).forEach(block => { + if (block.type === this.type && block.id !== this.id) { + block.dispose(); + } + }); + } + } + }, +}; + +Blockly.JavaScript.before_purchase = block => { + const stack = Blockly.JavaScript.statementToCode(block, 'BEFOREPURCHASE_STACK'); + + const code = `BinaryBotPrivateBeforePurchase = function BinaryBotPrivateBeforePurchase() { + ${stack} + };\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Before Purchase/index.js b/src/scratch/blocks/Binary/Before Purchase/index.js new file mode 100755 index 00000000..78e92d0a --- /dev/null +++ b/src/scratch/blocks/Binary/Before Purchase/index.js @@ -0,0 +1,4 @@ +import './before_purchase'; +import './purchase'; +import './ask_price'; +import './payout'; diff --git a/src/scratch/blocks/Binary/Before Purchase/payout.js b/src/scratch/blocks/Binary/Before Purchase/payout.js new file mode 100755 index 00000000..1c72825a --- /dev/null +++ b/src/scratch/blocks/Binary/Before Purchase/payout.js @@ -0,0 +1,45 @@ +import { getPurchaseChoices } from '../../../shared'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.payout = { + init() { + this.jsonInit({ + message0: translate('Payout %1'), + args0 : [ + { + type : 'field_dropdown', + name : 'PURCHASE_LIST', + options: getPurchaseChoices, + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Payout for selected proposal'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + if (this.isDescendantOf('before_purchase')) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.payout = block => { + const purchaseList = block.getFieldValue('PURCHASE_LIST'); + + const code = `Bot.getPayout('${purchaseList}')`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Before Purchase/purchase.js b/src/scratch/blocks/Binary/Before Purchase/purchase.js new file mode 100755 index 00000000..360db245 --- /dev/null +++ b/src/scratch/blocks/Binary/Before Purchase/purchase.js @@ -0,0 +1,69 @@ +import { getPurchaseChoices, updatePurchaseChoices } from '../../../shared'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.purchase = { + init() { + this.jsonInit({ + message0: translate('Purchase %1'), + args0 : [ + { + type : 'field_dropdown', + name : 'PURCHASE_LIST', + options: getPurchaseChoices, + }, + ], + previousStatement: null, + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Payout for selected proposal'), + }); + + // Ensure one of this type per statement-stack + this.setNextStatement(false); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + if (this.isDescendantOf('before_purchase')) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } else if (event.type === Blockly.Events.BLOCK_CHANGE || Blockly.Events.BLOCK_CREATE) { + const tradeDefinitionBlock = this.workspace + .getAllBlocks(true) + .find(block => block.type === 'trade_definition'); + + if (!tradeDefinitionBlock) { + return; + } + + const tradeTypeBlock = tradeDefinitionBlock.getChildByType('trade_definition_tradetype'); + if (!tradeTypeBlock) { + return; + } + + const tradeType = tradeTypeBlock.getFieldValue('TRADETYPE_LIST'); + const contractTypeBlock = tradeDefinitionBlock.getChildByType('trade_definition_contracttype'); + const contractType = contractTypeBlock.getFieldValue('TYPE_LIST'); + const oppositesName = tradeType.toUpperCase(); + + if (tradeType && contractType && oppositesName) { + updatePurchaseChoices(contractType, oppositesName); + } + } + }, +}; + +Blockly.JavaScript.purchase = block => { + const purchaseList = block.getFieldValue('PURCHASE_LIST'); + + const code = `Bot.purchase('${purchaseList}');\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/During Purchase/check_sell.js b/src/scratch/blocks/Binary/During Purchase/check_sell.js new file mode 100755 index 00000000..f51196a7 --- /dev/null +++ b/src/scratch/blocks/Binary/During Purchase/check_sell.js @@ -0,0 +1,35 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.check_sell = { + init() { + this.jsonInit({ + message0 : translate('Sell is available'), + output : 'Boolean', + outputShape : Blockly.OUTPUT_SHAPE_HEXAGONAL, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('True if sell at market is available'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + if (this.isDescendantOf('during_purchase')) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.check_sell = () => { + const code = 'Bot.isSellAvailable()'; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/During Purchase/during_purchase.js b/src/scratch/blocks/Binary/During Purchase/during_purchase.js new file mode 100755 index 00000000..a373ffb9 --- /dev/null +++ b/src/scratch/blocks/Binary/During Purchase/during_purchase.js @@ -0,0 +1,63 @@ +import { sellContract } from '../../images'; +import { setBlockTextColor } from '../../../utils'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.during_purchase = { + init() { + this.jsonInit({ + message0: translate('%1 (3) Watch and sell your purchased contract %2'), + message1: '%1', + args0 : [ + { + type : 'field_image', + src : sellContract, + width : 25, + height: 25, + alt : 'S', + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_statement', + name : 'DURING_PURCHASE_STACK', + check: 'SellAtMarket', + }, + ], + colour : '#2a3052', + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate( + 'Watch the purchased contract info and sell at market if available (Runs on contract update)' + ), + }); + }, + onchange(event) { + setBlockTextColor(this); + if (!this.workspace || this.isInFlyout) { + return; + } + + // Maintain single instance of this block + if (event.type === Blockly.Events.BLOCK_CREATE) { + if (event.ids && event.ids.includes(this.id)) { + this.workspace.getAllBlocks(true).forEach(block => { + if (block.type === this.type && block.id !== this.id) { + block.dispose(); + } + }); + } + } + }, +}; + +Blockly.JavaScript.during_purchase = block => { + const stack = Blockly.JavaScript.statementToCode(block, 'DURING_PURCHASE_STACK'); + + const code = `BinaryBotPrivateDuringPurchase = function BinaryBotPrivateDuringPurchase() { + ${stack} + };\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/During Purchase/index.js b/src/scratch/blocks/Binary/During Purchase/index.js new file mode 100755 index 00000000..66059bd2 --- /dev/null +++ b/src/scratch/blocks/Binary/During Purchase/index.js @@ -0,0 +1,4 @@ +import './during_purchase'; +import './sell_at_market'; +import './check_sell'; +import './sell_price'; diff --git a/src/scratch/blocks/Binary/During Purchase/sell_at_market.js b/src/scratch/blocks/Binary/During Purchase/sell_at_market.js new file mode 100755 index 00000000..073e63e2 --- /dev/null +++ b/src/scratch/blocks/Binary/During Purchase/sell_at_market.js @@ -0,0 +1,32 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.sell_at_market = { + init() { + this.jsonInit({ + message0 : translate('Sell at market'), + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + tooltip : translate('Sell at market'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + if (this.isDescendantOf('during_purchase')) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.sell_at_market = () => 'Bot.sellAtMarket();\n'; diff --git a/src/scratch/blocks/Binary/During Purchase/sell_price.js b/src/scratch/blocks/Binary/During Purchase/sell_price.js new file mode 100755 index 00000000..341cf541 --- /dev/null +++ b/src/scratch/blocks/Binary/During Purchase/sell_price.js @@ -0,0 +1,35 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.sell_price = { + init() { + this.jsonInit({ + message0 : translate('Sell profit/loss'), + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Returns the profit for sell at market.'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + if (this.isDescendantOf('during_purchase')) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.sell_price = () => { + const code = 'Bot.getSellPrice()'; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Indicators/Parts/fast_ema_period.js b/src/scratch/blocks/Binary/Indicators/Parts/fast_ema_period.js new file mode 100755 index 00000000..98335499 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/Parts/fast_ema_period.js @@ -0,0 +1,28 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.fast_ema_period = { + init() { + this.jsonInit({ + message0: translate('Fast EMA Period %1'), + args0 : [ + { + type : 'input_value', + name : 'FAST_EMA_PERIOD', + check: null, + }, + ], + colour : Blockly.Colours.BinaryLessGray.colour, + colourSecondary : Blockly.Colours.BinaryLessGray.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessGray.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + this.setMovable(false); + this.setDeletable(false); + }, + onchange : Blockly.Blocks.input_list.onchange, + allowedParents: ['macda_statement'], +}; + +Blockly.JavaScript.fast_ema_period = () => {}; diff --git a/src/scratch/blocks/Binary/Indicators/Parts/index.js b/src/scratch/blocks/Binary/Indicators/Parts/index.js new file mode 100755 index 00000000..b3889637 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/Parts/index.js @@ -0,0 +1,7 @@ +import './input_list'; +import './period'; +import './std_dev_multiplier_up'; +import './std_dev_multiplier_down'; +import './fast_ema_period'; +import './slow_ema_period'; +import './signal_ema_period'; diff --git a/src/scratch/blocks/Binary/Indicators/Parts/input_list.js b/src/scratch/blocks/Binary/Indicators/Parts/input_list.js new file mode 100755 index 00000000..8152054d --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/Parts/input_list.js @@ -0,0 +1,64 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.input_list = { + init() { + this.requiredParentId = ''; + + this.jsonInit({ + message0: translate('Input List %1'), + args0 : [ + { + type : 'input_value', + name : 'INPUT_LIST', + check: 'Array', + }, + ], + colour : Blockly.Colours.BinaryLessGray.colour, + colourSecondary : Blockly.Colours.BinaryLessGray.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessGray.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + this.setMovable(false); + this.setDeletable(false); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + const surroundParent = this.getSurroundParent(); + if (event.type === Blockly.Events.END_DRAG) { + if (!this.requiredParentId && this.allowedParents.includes(surroundParent.type)) { + this.requiredParentId = surroundParent.id; + } else if (!surroundParent || surroundParent.id !== this.requiredParentId) { + Blockly.Events.disable(); + this.unplug(false); + + const parentBlock = this.workspace.getAllBlocks().find(block => block.id === this.requiredParentId); + + if (parentBlock) { + const parentConnection = parentBlock.getLastConnectionInStatement('STATEMENT'); + parentConnection.connect(this.previousConnection); + } else { + this.dispose(); + } + Blockly.Events.enable(); + } + } + }, + allowedParents: [ + 'bb_statement', + 'bba_statement', + 'ema_statement', + 'emaa_statement', + 'macda_statement', + 'rsi_statement', + 'rsia_statement', + 'sma_statement', + 'smaa_statement', + ], +}; + +Blockly.JavaScript.input_list = () => {}; diff --git a/src/scratch/blocks/Binary/Indicators/Parts/period.js b/src/scratch/blocks/Binary/Indicators/Parts/period.js new file mode 100755 index 00000000..b9ab40a5 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/Parts/period.js @@ -0,0 +1,38 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.period = { + init() { + this.jsonInit({ + message0: translate('Period %1'), + args0 : [ + { + type : 'input_value', + name : 'PERIOD', + check: null, + }, + ], + colour : Blockly.Colours.BinaryLessGray.colour, + colourSecondary : Blockly.Colours.BinaryLessGray.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessGray.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + this.setMovable(false); + this.setDeletable(false); + }, + onchange : Blockly.Blocks.input_list.onchange, + allowedParents: [ + 'bb_statement', + 'bba_statement', + 'ema_statement', + 'emaa_statement', + 'macda_statement', + 'rsi_statement', + 'rsia_statement', + 'sma_statement', + 'smaa_statement', + ], +}; + +Blockly.JavaScript.period = () => {}; diff --git a/src/scratch/blocks/Binary/Indicators/Parts/signal_ema_period.js b/src/scratch/blocks/Binary/Indicators/Parts/signal_ema_period.js new file mode 100755 index 00000000..36505aaa --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/Parts/signal_ema_period.js @@ -0,0 +1,28 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.signal_ema_period = { + init() { + this.jsonInit({ + message0: translate('Signal EMA Period %1'), + args0 : [ + { + type : 'input_value', + name : 'SIGNAL_EMA_PERIOD', + check: null, + }, + ], + colour : Blockly.Colours.BinaryLessGray.colour, + colourSecondary : Blockly.Colours.BinaryLessGray.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessGray.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + this.setMovable(false); + this.setDeletable(false); + }, + onchange : Blockly.Blocks.input_list.onchange, + allowedParents: ['macda_statement'], +}; + +Blockly.JavaScript.signal_ema_period = () => {}; diff --git a/src/scratch/blocks/Binary/Indicators/Parts/slow_ema_period.js b/src/scratch/blocks/Binary/Indicators/Parts/slow_ema_period.js new file mode 100755 index 00000000..3cb19cf2 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/Parts/slow_ema_period.js @@ -0,0 +1,28 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.slow_ema_period = { + init() { + this.jsonInit({ + message0: translate('Slow EMA Period %1'), + args0 : [ + { + type : 'input_value', + name : 'SLOW_EMA_PERIOD', + check: null, + }, + ], + colour : Blockly.Colours.BinaryLessGray.colour, + colourSecondary : Blockly.Colours.BinaryLessGray.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessGray.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + this.setMovable(false); + this.setDeletable(false); + }, + onchange : Blockly.Blocks.input_list.onchange, + allowedParents: ['macda_statement'], +}; + +Blockly.JavaScript.slow_ema_period = () => {}; diff --git a/src/scratch/blocks/Binary/Indicators/Parts/std_dev_multiplier_down.js b/src/scratch/blocks/Binary/Indicators/Parts/std_dev_multiplier_down.js new file mode 100755 index 00000000..b4f48382 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/Parts/std_dev_multiplier_down.js @@ -0,0 +1,25 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.std_dev_multiplier_down = { + init() { + this.jsonInit({ + message0: translate('Standard Deviation Down Multiplier %1'), + args0 : [ + { + type : 'input_value', + name : 'DOWNMULTIPLIER', + check: null, + }, + ], + colour : Blockly.Colours.BinaryLessGray.colour, + colourSecondary : Blockly.Colours.BinaryLessGray.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessGray.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, + onchange : Blockly.Blocks.input_list.onchange, + allowedParents: ['bb_statement', 'bba_statement'], +}; + +Blockly.JavaScript.std_dev_multiplier_down = () => {}; diff --git a/src/scratch/blocks/Binary/Indicators/Parts/std_dev_multiplier_up.js b/src/scratch/blocks/Binary/Indicators/Parts/std_dev_multiplier_up.js new file mode 100755 index 00000000..bdf53e27 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/Parts/std_dev_multiplier_up.js @@ -0,0 +1,28 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.std_dev_multiplier_up = { + init() { + this.jsonInit({ + message0: translate('Standard Deviation Up Multiplier %1'), + args0 : [ + { + type : 'input_value', + name : 'UPMULTIPLIER', + check: null, + }, + ], + colour : Blockly.Colours.BinaryLessGray.colour, + colourSecondary : Blockly.Colours.BinaryLessGray.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessGray.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + this.setMovable(false); + this.setDeletable(false); + }, + onchange : Blockly.Blocks.input_list.onchange, + allowedParents: ['bb_statement', 'bba_statement'], +}; + +Blockly.JavaScript.std_dev_multiplier_up = () => {}; diff --git a/src/scratch/blocks/Binary/Indicators/bb_statement.js b/src/scratch/blocks/Binary/Indicators/bb_statement.js new file mode 100755 index 00000000..03db7bcb --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/bb_statement.js @@ -0,0 +1,73 @@ +import { expectValue } from '../../../shared'; +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.bb_statement = { + init() { + this.jsonInit({ + message0: translate('set %1 to Bollinger Bands %2 %3'), + message1: '%1', + args0 : [ + { + type : 'field_variable', + name : 'VARIABLE', + variable: 'bb', + }, + { + type : 'field_dropdown', + name : 'BBRESULT_LIST', + options: config.bbResult, + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_statement', + name : 'STATEMENT', + check: null, + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Calculates Bollinger Bands (BB) from a list with a period'), + previousStatement: null, + nextStatement : null, + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + const blocksInStatement = this.getBlocksInStatement('STATEMENT'); + blocksInStatement.forEach(block => { + if (!this.requiredParamBlocks.includes(block.type)) { + Blockly.Events.disable(); + block.unplug(false); + Blockly.Events.enable(); + } + }); + } + }, + requiredParamBlocks: ['input_list', 'period', 'std_dev_multiplier_up', 'std_dev_multiplier_down'], +}; + +Blockly.JavaScript.bb_statement = block => { + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('VARIABLE'), + Blockly.Variables.NAME_TYPE + ); + const bbResult = block.getFieldValue('BBRESULT_LIST'); + const input = expectValue(block.getChildByType('input_list'), 'INPUT_LIST'); + const period = block.childValueToCode('period', 'PERIOD') || '10'; + const stdDevUp = block.childValueToCode('std_dev_multiplier_up', 'UPMULTIPLIER') || '5'; + const stdDevDown = block.childValueToCode('std_dev_multiplier_down', 'DOWNMULTIPLIER') || '5'; + + const code = `${varName} = Bot.bb(${input}, { periods: ${period}, stdDevUp: ${stdDevUp}, stdDevDown: ${stdDevDown} }, ${bbResult});\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Indicators/bba_statement.js b/src/scratch/blocks/Binary/Indicators/bba_statement.js new file mode 100755 index 00000000..c3f065b4 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/bba_statement.js @@ -0,0 +1,58 @@ +import { expectValue } from '../../../shared'; +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.bba_statement = { + init() { + this.jsonInit({ + message0: translate('set %1 to Bollinger Bands Array %2 %3'), + message1: '%1', + args0 : [ + { + type : 'field_variable', + name : 'VARIABLE', + variable: 'bba', + }, + { + type : 'field_dropdown', + name : 'BBRESULT_LIST', + options: config.bbResult, + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_statement', + name : 'STATEMENT', + check: null, + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Calculates Bollinger Bands (BB) list from a list with a period'), + previousStatement: null, + nextStatement : null, + }); + }, + onchange : Blockly.Blocks.bb_statement.onchange, + requiredParamBlocks: ['input_list', 'period', 'std_dev_multiplier_up', 'std_dev_multiplier_down'], +}; + +Blockly.JavaScript.bba_statement = block => { + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('VARIABLE'), + Blockly.Variables.NAME_TYPE + ); + const bbResult = block.getFieldValue('BBRESULT_LIST'); + const input = expectValue(block.getChildByType('input_list'), 'INPUT_LIST'); + const period = block.childValueToCode('period', 'PERIOD') || '10'; + const stdDevUp = block.childValueToCode('std_dev_multiplier_up', 'UPMULTIPLIER') || '5'; + const stdDevDown = block.childValueToCode('std_dev_multiplier_down', 'DOWNMULTIPLIER') || '5'; + + const code = `${varName} = Bot.bba(${input}, { periods: ${period}, stdDevUp: ${stdDevUp}, stdDevDown: ${stdDevDown} }, ${bbResult});\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Indicators/ema_statement.js b/src/scratch/blocks/Binary/Indicators/ema_statement.js new file mode 100755 index 00000000..95cc508b --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/ema_statement.js @@ -0,0 +1,49 @@ +import { expectValue } from '../../../shared'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.ema_statement = { + init() { + this.jsonInit({ + message0: translate('set %1 to Exponentional Moving Average %2'), + message1: '%1', + args0 : [ + { + type : 'field_variable', + name : 'VARIABLE', + variable: 'ema', + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_statement', + name : 'STATEMENT', + check: null, + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Calculates Exponential Moving Average (EMA) from a list with a period'), + previousStatement: null, + nextStatement : null, + }); + }, + onchange : Blockly.Blocks.bb_statement.onchange, + requiredParamBlocks: ['input_list', 'period'], +}; + +Blockly.JavaScript.ema_statement = block => { + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('VARIABLE'), + Blockly.Variables.NAME_TYPE + ); + const input = expectValue(block.getChildByType('input_list'), 'INPUT_LIST'); + const period = block.childValueToCode('period', 'PERIOD') || '10'; + + const code = `${varName} = Bot.ema(${input}, ${period});\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Indicators/emaa_statement.js b/src/scratch/blocks/Binary/Indicators/emaa_statement.js new file mode 100755 index 00000000..c4365ca2 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/emaa_statement.js @@ -0,0 +1,49 @@ +import { expectValue } from '../../../shared'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.emaa_statement = { + init() { + this.jsonInit({ + message0: translate('set %1 to Exponentional Moving Average Array %2'), + message1: '%1', + args0 : [ + { + type : 'field_variable', + name : 'VARIABLE', + variable: 'emaa', + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_statement', + name : 'STATEMENT', + check: null, + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Calculates Exponential Moving Average (EMA) list from a list of values with a period'), + previousStatement: null, + nextStatement : null, + }); + }, + onchange : Blockly.Blocks.bb_statement.onchange, + requiredParamBlocks: ['input_list', 'period'], +}; + +Blockly.JavaScript.emaa_statement = block => { + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('VARIABLE'), + Blockly.Variables.NAME_TYPE + ); + const input = expectValue(block.getChildByType('input_list'), 'INPUT_LIST'); + const period = block.childValueToCode('period', 'PERIOD') || '10'; + + const code = `${varName} = Bot.emaa(${input}, ${period});\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Indicators/index.js b/src/scratch/blocks/Binary/Indicators/index.js new file mode 100755 index 00000000..ff62324e --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/index.js @@ -0,0 +1,10 @@ +import './bb_statement'; +import './bba_statement'; +import './ema_statement'; +import './emaa_statement'; +import './rsi_statement'; +import './rsia_statement'; +import './sma_statement'; +import './smaa_statement'; +import './macda_statement'; +import './Parts'; diff --git a/src/scratch/blocks/Binary/Indicators/macda_statement.js b/src/scratch/blocks/Binary/Indicators/macda_statement.js new file mode 100755 index 00000000..2c0f9d69 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/macda_statement.js @@ -0,0 +1,62 @@ +import { expectValue } from '../../../shared'; +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.macda_statement = { + init() { + this.jsonInit({ + message0: translate('set %1 to MACD Array %2 %3'), + message1: '%1', + args0 : [ + { + type : 'field_variable', + name : 'VARIABLE', + variable: 'macda', + }, + { + type : 'field_dropdown', + name : 'MACDFIELDS_LIST', + options: config.macdFields, + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_statement', + name : 'STATEMENT', + check: null, + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Calculates Moving Average Convergence Divergence (MACD) list from a list'), + previousStatement: null, + nextStatement : null, + }); + }, + onchange : Blockly.Blocks.bb_statement.onchange, + requiredParamBlocks: ['input_list', 'fast_ema_period', 'slow_ema_period', 'signal_ema_period'], +}; + +Blockly.JavaScript.macda_statement = block => { + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('VARIABLE'), + Blockly.Variables.NAME_TYPE + ); + const macdField = block.getFieldValue('MACDFIELDS_LIST'); + const input = expectValue(block.getChildByType('input_list'), 'INPUT_LIST'); + const fastEmaPeriod = block.childValueToCode('fast_ema_period', 'FAST_EMA_PERIOD') || '12'; + const slowEmaPeriod = block.childValueToCode('slow_ema_period', 'SLOW_EMA_PERIOD') || '26'; + const signalEmaPeriod = block.childValueToCode('signal_ema_period', 'SIGNAL_EMA_PERIOD') || '9'; + + const code = `${varName} = Bot.macda(${input}, { + fastEmaPeriod: ${fastEmaPeriod}, + slowEmaPeriod: ${slowEmaPeriod}, + signalEmaPeriod: ${signalEmaPeriod}, + }, ${macdField});\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Indicators/rsi_statement.js b/src/scratch/blocks/Binary/Indicators/rsi_statement.js new file mode 100755 index 00000000..0c282aa4 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/rsi_statement.js @@ -0,0 +1,49 @@ +import { expectValue } from '../../../shared'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.rsi_statement = { + init() { + this.jsonInit({ + message0: translate('set %1 to Relative Strength Index %2'), + message1: '%1', + args0 : [ + { + type : 'field_variable', + name : 'VARIABLE', + variable: 'rsi', + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_statement', + name : 'STATEMENT', + check: null, + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Relative Strength Index (RSI) from a list with a period'), + previousStatement: null, + nextStatement : null, + }); + }, + onchange : Blockly.Blocks.bb_statement.onchange, + requiredParamBlocks: ['input_list', 'period'], +}; + +Blockly.JavaScript.rsi_statement = block => { + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('VARIABLE'), + Blockly.Variables.NAME_TYPE + ); + const input = expectValue(block.getChildByType('input_list'), 'INPUT_LIST'); + const period = block.childValueToCode('period', 'PERIOD') || '10'; + + const code = `${varName} = Bot.rsi(${input}, ${period});\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Indicators/rsia_statement.js b/src/scratch/blocks/Binary/Indicators/rsia_statement.js new file mode 100755 index 00000000..5a8300e3 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/rsia_statement.js @@ -0,0 +1,49 @@ +import { expectValue } from '../../../shared'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.rsia_statement = { + init() { + this.jsonInit({ + message0: translate('set %1 to Relative Strength Index Array %2'), + message1: '%1', + args0 : [ + { + type : 'field_variable', + name : 'VARIABLE', + variable: 'rsia', + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_statement', + name : 'STATEMENT', + check: null, + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Calculates Relative Strength Index (RSI) list from a list of values with a period'), + previousStatement: null, + nextStatement : null, + }); + }, + onchange : Blockly.Blocks.bb_statement.onchange, + requiredParamBlocks: ['input_list', 'period'], +}; + +Blockly.JavaScript.rsia_statement = block => { + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('VARIABLE'), + Blockly.Variables.NAME_TYPE + ); + const input = expectValue(block.getChildByType('input_list'), 'INPUT_LIST'); + const period = block.childValueToCode('period', 'PERIOD') || '10'; + + const code = `${varName} = Bot.rsia(${input}, ${period});\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Indicators/sma_statement.js b/src/scratch/blocks/Binary/Indicators/sma_statement.js new file mode 100755 index 00000000..89d79736 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/sma_statement.js @@ -0,0 +1,49 @@ +import { expectValue } from '../../../shared'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.sma_statement = { + init() { + this.jsonInit({ + message0: translate('set %1 to Simple Moving Average %2'), + message1: '%1', + args0 : [ + { + type : 'field_variable', + name : 'VARIABLE', + variable: 'sma', + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_statement', + name : 'STATEMENT', + check: null, + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Calculates Simple Moving Average (SMA) from a list with a period'), + previousStatement: null, + nextStatement : null, + }); + }, + onchange : Blockly.Blocks.bb_statement.onchange, + requiredParamBlocks: ['input_list', 'period'], +}; + +Blockly.JavaScript.sma_statement = block => { + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('VARIABLE'), + Blockly.Variables.NAME_TYPE + ); + const input = expectValue(block.getChildByType('input_list'), 'INPUT_LIST'); + const period = block.getChildFieldValue('period', 'PERIOD') || '10'; + + const code = `${varName} = Bot.sma(${input}, ${period});\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Indicators/smaa_statement.js b/src/scratch/blocks/Binary/Indicators/smaa_statement.js new file mode 100755 index 00000000..4ebab4a8 --- /dev/null +++ b/src/scratch/blocks/Binary/Indicators/smaa_statement.js @@ -0,0 +1,48 @@ +import { expectValue } from '../../../shared'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.smaa_statement = { + init() { + this.jsonInit({ + message0: translate('set %1 to Simple Moving Average Array %2'), + message1: '%1', + args0 : [ + { + type : 'field_variable', + name : 'VARIABLE', + variable: 'smaa', + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type : 'input_statement', + name : 'STATEMENT', + check: null, + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Calculates Simple Moving Average (SMA) from a list with a period'), + previousStatement: null, + nextStatement : null, + }); + }, + onchange: Blockly.Blocks.bb_statement.onchange, +}; + +Blockly.JavaScript.smaa_statement = block => { + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('VARIABLE'), + Blockly.Variables.NAME_TYPE + ); + const input = expectValue(block.getChildByType('input_list'), 'INPUT_LIST'); + const period = block.childValueToCode('period', 'PERIOD') || '10'; + + const code = `${varName} = Bot.smaa(${input}, ${period});\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Tick Analysis/check_direction.js b/src/scratch/blocks/Binary/Tick Analysis/check_direction.js new file mode 100755 index 00000000..f675ad7a --- /dev/null +++ b/src/scratch/blocks/Binary/Tick Analysis/check_direction.js @@ -0,0 +1,52 @@ +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.check_direction = { + init() { + this.jsonInit({ + message0: translate('Direction is %1'), + args0 : [ + { + type : 'field_dropdown', + name : 'CHECK_DIRECTION', + options: config.lists.CHECK_DIRECTION, + }, + ], + output : 'Boolean', + outputShape : Blockly.OUTPUT_SHAPE_HEXAGONAL, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('True if the direction matches the selection'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + const allowedScopes = [ + 'trade_definition', + 'during_purchase', + 'before_purchase', + 'after_purchase', + 'tick_analysis', + ]; + if (allowedScopes.some(scope => this.isDescendantOf(scope))) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.check_direction = block => { + const checkWith = block.getFieldValue('CHECK_DIRECTION'); + + const code = `Bot.checkDirection('${checkWith}')`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Tick Analysis/get_ohlc.js b/src/scratch/blocks/Binary/Tick Analysis/get_ohlc.js new file mode 100755 index 00000000..09810aa4 --- /dev/null +++ b/src/scratch/blocks/Binary/Tick Analysis/get_ohlc.js @@ -0,0 +1,62 @@ +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.get_ohlc = { + init() { + this.jsonInit({ + message0: translate('in candle list get # from end %1'), + message1: translate('with interval: %1'), + args0 : [ + { + type : 'input_value', + name : 'CANDLEINDEX', + check: 'Number', + }, + ], + args1: [ + { + type : 'field_dropdown', + name : 'CANDLEINTERVAL_LIST', + options: config.candleIntervals, + }, + ], + output : 'Candle', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Get the nth recent candle'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + const allowedScopes = [ + 'trade_definition', + 'during_purchase', + 'before_purchase', + 'after_purchase', + 'tick_analysis', + ]; + if (allowedScopes.some(scope => this.isDescendantOf(scope))) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.get_ohlc = block => { + const selectedGranularity = block.getFieldValue('CANDLEINTERVAL_LIST'); + const granularity = selectedGranularity === 'default' ? 'undefined' : selectedGranularity; + const index = Blockly.JavaScript.valueToCode(block, 'CANDLEINDEX', Blockly.JavaScript.ORDER_ATOMIC) || '1'; + + const code = `Bot.getOhlcFromEnd({ index: ${index}, granularity: ${granularity} })`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Tick Analysis/index.js b/src/scratch/blocks/Binary/Tick Analysis/index.js new file mode 100755 index 00000000..089bfcf9 --- /dev/null +++ b/src/scratch/blocks/Binary/Tick Analysis/index.js @@ -0,0 +1,10 @@ +import './tick'; +import './ticks'; +import './ohlc'; +import './ohlc_values'; +import './readOhlc'; +import './get_ohlc'; +import './check_direction'; +import './tick_analysis'; +import './last_digit'; +import './lastDigitList'; diff --git a/src/scratch/blocks/Binary/Tick Analysis/lastDigitList.js b/src/scratch/blocks/Binary/Tick Analysis/lastDigitList.js new file mode 100755 index 00000000..bf981718 --- /dev/null +++ b/src/scratch/blocks/Binary/Tick Analysis/lastDigitList.js @@ -0,0 +1,39 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.lastDigitList = { + init() { + this.jsonInit({ + message0 : translate('Last Digit List'), + output : 'Array', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Returns the list of last digit values'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + const allowedScopes = [ + 'trade_definition', + 'during_purchase', + 'before_purchase', + 'after_purchase', + 'tick_analysis', + ]; + if (allowedScopes.some(scope => this.isDescendantOf(scope))) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.lastDigitList = () => ['Bot.getLastDigitList()', Blockly.JavaScript.ORDER_ATOMIC]; diff --git a/src/scratch/blocks/Binary/Tick Analysis/last_digit.js b/src/scratch/blocks/Binary/Tick Analysis/last_digit.js new file mode 100755 index 00000000..3ea26998 --- /dev/null +++ b/src/scratch/blocks/Binary/Tick Analysis/last_digit.js @@ -0,0 +1,39 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.last_digit = { + init() { + this.jsonInit({ + message0 : translate('Last Digit'), + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Returns the last digit of the latest tick'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + const allowedScopes = [ + 'trade_definition', + 'during_purchase', + 'before_purchase', + 'after_purchase', + 'tick_analysis', + ]; + if (allowedScopes.some(scope => this.isDescendantOf(scope))) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.last_digit = () => ['Bot.getLastDigit()', Blockly.JavaScript.ORDER_ATOMIC]; diff --git a/src/scratch/blocks/Binary/Tick Analysis/ohlc.js b/src/scratch/blocks/Binary/Tick Analysis/ohlc.js new file mode 100755 index 00000000..118615a2 --- /dev/null +++ b/src/scratch/blocks/Binary/Tick Analysis/ohlc.js @@ -0,0 +1,54 @@ +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.ohlc = { + init() { + this.jsonInit({ + message0: translate('Candles List'), + message1: translate('with interval: %1'), + args1 : [ + { + type : 'field_dropdown', + name : 'CANDLEINTERVAL_LIST', + options: config.candleIntervals, + }, + ], + output : 'Array', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Returns the candle list'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + const allowedScopes = [ + 'trade_definition', + 'during_purchase', + 'before_purchase', + 'after_purchase', + 'tick_analysis', + ]; + if (allowedScopes.some(scope => this.isDescendantOf(scope))) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.ohlc = block => { + const selectedGranularity = block.getFieldValue('CANDLEINTERVAL_LIST'); + const granularity = selectedGranularity === 'default' ? 'undefined' : selectedGranularity; + + const code = `Bot.getOhlc({ granularity: ${granularity} })`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Tick Analysis/ohlc_values.js b/src/scratch/blocks/Binary/Tick Analysis/ohlc_values.js new file mode 100755 index 00000000..c988ef85 --- /dev/null +++ b/src/scratch/blocks/Binary/Tick Analysis/ohlc_values.js @@ -0,0 +1,59 @@ +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.ohlc_values = { + init() { + this.jsonInit({ + message0: translate('Make a List of %1 values in candles list with interval: %2'), + args0 : [ + { + type : 'field_dropdown', + name : 'OHLCFIELD_LIST', + options: config.ohlcFields, + }, + { + type : 'field_dropdown', + name : 'CANDLEINTERVAL_LIST', + options: config.candleIntervals, + }, + ], + output : 'Array', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Returns a list of the selected candle values'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + const allowedScopes = [ + 'trade_definition', + 'during_purchase', + 'before_purchase', + 'after_purchase', + 'tick_analysis', + ]; + if (allowedScopes.some(scope => this.isDescendantOf(scope))) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.ohlc_values = block => { + const selectedGranularity = block.getFieldValue('CANDLEINTERVAL_LIST'); + const granularity = selectedGranularity === 'default' ? 'undefined' : selectedGranularity; + const ohlcField = block.getFieldValue('OHLCFIELD_LIST'); + + const code = `Bot.getOhlc({ field: '${ohlcField}', granularity: ${granularity} })`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Tick Analysis/readOhlc.js b/src/scratch/blocks/Binary/Tick Analysis/readOhlc.js new file mode 100755 index 00000000..217694e7 --- /dev/null +++ b/src/scratch/blocks/Binary/Tick Analysis/readOhlc.js @@ -0,0 +1,68 @@ +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.read_ohlc = { + init() { + this.jsonInit({ + message0: translate('In candles list read %1 from end %2'), + message1: translate('with interval: %1'), + args0 : [ + { + type : 'field_dropdown', + name : 'OHLCFIELD_LIST', + options: config.ohlcFields, + }, + { + type : 'input_value', + name : 'CANDLEINDEX', + check: 'Number', + }, + ], + args1: [ + { + type : 'field_dropdown', + name : 'CANDLEINTERVAL_LIST', + options: config.candleIntervals, + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Read the selected candle value in the nth recent candle'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + const allowedScopes = [ + 'trade_definition', + 'during_purchase', + 'before_purchase', + 'after_purchase', + 'tick_analysis', + ]; + if (allowedScopes.some(scope => this.isDescendantOf(scope))) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.read_ohlc = block => { + const selectedGranularity = block.getFieldValue('CANDLEINTERVAL_LIST'); + const granularity = selectedGranularity === 'default' ? 'undefined' : selectedGranularity; + const ohlcField = block.getFieldValue('OHLCFIELD_LIST'); + const index = Blockly.JavaScript.valueToCode(block, 'CANDLEINDEX', Blockly.JavaScript.ORDER_ATOMIC) || '1'; + + const code = `Bot.getOhlcFromEnd({ field: '${ohlcField}', index: ${index}, granularity: ${granularity} })`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Tick Analysis/tick.js b/src/scratch/blocks/Binary/Tick Analysis/tick.js new file mode 100755 index 00000000..6db85d18 --- /dev/null +++ b/src/scratch/blocks/Binary/Tick Analysis/tick.js @@ -0,0 +1,39 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.tick = { + init() { + this.jsonInit({ + message0 : translate('Last Tick'), + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Returns the tick value received by a before purchase block'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + const allowedScopes = [ + 'trade_definition', + 'during_purchase', + 'before_purchase', + 'after_purchase', + 'tick_analysis', + ]; + if (allowedScopes.some(scope => this.isDescendantOf(scope))) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.tick = () => ['Bot.getLastTick()', Blockly.JavaScript.ORDER_ATOMIC]; diff --git a/src/scratch/blocks/Binary/Tick Analysis/tick_analysis.js b/src/scratch/blocks/Binary/Tick Analysis/tick_analysis.js new file mode 100755 index 00000000..9b19d529 --- /dev/null +++ b/src/scratch/blocks/Binary/Tick Analysis/tick_analysis.js @@ -0,0 +1,33 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.tick_analysis = { + init() { + this.jsonInit({ + message0: translate('This block is called on every tick %1 %2'), + args0 : [ + { + type: 'input_dummy', + }, + { + type : 'input_statement', + name : 'TICKANALYSIS_STACK', + check: null, + }, + ], + colour : '#fef1cf', + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('You can use this block to analyze the ticks, regardless of your trades'), + }); + }, +}; + +Blockly.JavaScript.tick_analysis = block => { + const stack = Blockly.JavaScript.statementToCode(block, 'TICKANALYSIS_STACK') || ''; + + const code = ` + BinaryBotPrivateTickAnalysisList.push(function BinaryBotPrivateTickAnalysis() { + ${stack} + });\n`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Tick Analysis/ticks.js b/src/scratch/blocks/Binary/Tick Analysis/ticks.js new file mode 100755 index 00000000..0fc84f39 --- /dev/null +++ b/src/scratch/blocks/Binary/Tick Analysis/ticks.js @@ -0,0 +1,39 @@ +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.ticks = { + init() { + this.jsonInit({ + message0 : translate('Ticks List'), + output : 'Array', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Returns the list of tick values'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + const allowedScopes = [ + 'trade_definition', + 'during_purchase', + 'before_purchase', + 'after_purchase', + 'tick_analysis', + ]; + if (allowedScopes.some(scope => this.isDescendantOf(scope))) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.ticks = () => ['Bot.getTicks()', Blockly.JavaScript.ORDER_ATOMIC]; diff --git a/src/scratch/blocks/Binary/Tools/Candle/index.js b/src/scratch/blocks/Binary/Tools/Candle/index.js new file mode 100755 index 00000000..80d5c9cb --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Candle/index.js @@ -0,0 +1,3 @@ +import './is_candle_black'; +import './ohlc_values_in_list'; +import './read_ohlc_obj'; diff --git a/src/scratch/blocks/Binary/Tools/Candle/is_candle_black.js b/src/scratch/blocks/Binary/Tools/Candle/is_candle_black.js new file mode 100755 index 00000000..6b2b5618 --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Candle/is_candle_black.js @@ -0,0 +1,31 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.is_candle_black = { + init() { + this.jsonInit({ + message0: translate('Is candle black? %1'), + args0 : [ + { + type : 'input_value', + name : 'OHLCOBJ', + check: 'Candle', + }, + ], + output : 'Boolean', + outputShape : Blockly.OUTPUT_SHAPE_HEXAGONAL, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate( + 'Checks if the given candle is black, returns true if close is less than open in the given candle.' + ), + }); + }, +}; + +Blockly.JavaScript.is_candle_black = block => { + const ohlcObj = Blockly.JavaScript.valueToCode(block, 'OHLCOBJ') || '{}'; + + const code = `Bot.isCandleBlack(${ohlcObj})`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Tools/Candle/ohlc_values_in_list.js b/src/scratch/blocks/Binary/Tools/Candle/ohlc_values_in_list.js new file mode 100755 index 00000000..e329810f --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Candle/ohlc_values_in_list.js @@ -0,0 +1,35 @@ +import config from '../../../../../constants/const'; +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.ohlc_values_in_list = { + init() { + this.jsonInit({ + message0: translate('Make a list of %1 values from candles list %2'), + args0 : [ + { + type : 'field_dropdown', + name : 'OHLCFIELD_LIST', + options: config.ohlcFields, + }, + { + type: 'input_value', + name: 'OHLCLIST', + }, + ], + output : 'Array', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Returns a list of the selected candle values'), + }); + }, +}; + +Blockly.JavaScript.ohlc_values_in_list = block => { + const ohlcField = block.getFieldValue('OHLCFIELD_LIST') || 'open'; + const ohlcList = Blockly.JavaScript.valueToCode(block, 'OHLCLIST') || '[]'; + + const code = `Bot.candleValues(${ohlcList}, '${ohlcField}')`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Tools/Candle/read_ohlc_obj.js b/src/scratch/blocks/Binary/Tools/Candle/read_ohlc_obj.js new file mode 100755 index 00000000..31ec0140 --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Candle/read_ohlc_obj.js @@ -0,0 +1,35 @@ +import config from '../../../../../constants/const'; +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.read_ohlc_obj = { + init() { + this.jsonInit({ + message0: translate('Read %1 value in candle %2'), + args0 : [ + { + type : 'field_dropdown', + name : 'OHLCFIELD_LIST', + options: config.ohlcFields, + }, + { + type: 'input_value', + name: 'OHLCOBJ', + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Read a field in a candle (read from the Candles list)'), + }); + }, +}; + +Blockly.JavaScript.read_ohlc_obj = block => { + const ohlcField = block.getFieldValue('OHLCFIELD_LIST'); + const ohlcObj = Blockly.JavaScript.valueToCode(block, 'OHLCOBJ') || '{}'; + + const code = `Bot.candleField(${ohlcObj}, '${ohlcField}')`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Tools/Misc./balance.js b/src/scratch/blocks/Binary/Tools/Misc./balance.js new file mode 100755 index 00000000..7be83f61 --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Misc./balance.js @@ -0,0 +1,43 @@ +import config from '../../../../../constants/const'; +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.balance = { + init() { + this.jsonInit({ + message0: translate('Balance: %1'), + args0 : [ + { + type : 'field_dropdown', + name : 'BALANCE_TYPE', + options: config.lists.BALANCE_TYPE, + }, + ], + output : null, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + + // Change shape based on selected type + const balanceTypeField = this.getField('BALANCE_TYPE'); + balanceTypeField.setValidator(value => { + if (value === 'STR') { + this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE); + this.setOutput(true, 'String'); + } else if (value === 'NUM') { + this.setOutputShape(Blockly.OUTPUT_SHAPE_ROUND); + this.setOutput(true, 'Number'); + } + this.initSvg(); + this.render(false); + return undefined; + }); + }, +}; + +Blockly.JavaScript.balance = block => { + const balanceType = block.getFieldValue('BALANCE_TYPE'); + + const code = `Bot.getBalance('${balanceType}')`; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Binary/Tools/Misc./block_holder.js b/src/scratch/blocks/Binary/Tools/Misc./block_holder.js new file mode 100755 index 00000000..a09592ed --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Misc./block_holder.js @@ -0,0 +1,25 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.block_holder = { + init() { + this.jsonInit({ + message0: translate('Blocks inside are ignored %1 %2'), + args0 : [ + { + type: 'input_dummy', + }, + { + type : 'input_statement', + name : 'USELESS_STACK', + check: null, + }, + ], + colour : '#fef1cf', + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Put your blocks in here to prevent them from being removed'), + }); + }, +}; + +Blockly.JavaScript.block_holder = () => ''; diff --git a/src/scratch/blocks/Binary/Tools/Misc./index.js b/src/scratch/blocks/Binary/Tools/Misc./index.js new file mode 100755 index 00000000..7aee0d7b --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Misc./index.js @@ -0,0 +1,6 @@ +import './balance'; +import './total_profit'; +import './total_runs'; +import './notify'; +import './loader'; +import './block_holder'; diff --git a/src/scratch/blocks/Binary/Tools/Misc./loader.js b/src/scratch/blocks/Binary/Tools/Misc./loader.js new file mode 100755 index 00000000..b6d66b0b --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Misc./loader.js @@ -0,0 +1,79 @@ +import { loadRemote } from '../../../../utils'; +import { translate } from '../../../../../utils/lang/i18n'; +import { observer as globalObserver } from '../../../../../utils/observer'; + +Blockly.Blocks.loader = { + init() { + this.loadedByMe = []; + this.loadedVariables = []; + this.currentUrl = ''; + + this.jsonInit({ + message0: translate('Load block from: %1'), + args0 : [ + { + type: 'field_input', + name: 'URL', + text: 'http://www.example.com/block.xml', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Load blocks from URL'), + }); + + const urlField = this.getField('URL'); + // eslint-disable-next-line no-underscore-dangle + urlField.onFinishEditing_ = newValue => this.onFinishEditingUrl(newValue); + }, + onFinishEditingUrl(newValue) { + if (this.currentUrl === newValue) { + return; + } + + if (this.disabled) { + const hasKnownUrl = this.workspace + .getAllBlocks() + .some(block => block.type === 'loader' && block.id !== this.id && block.currentUrl === this.currentUrl); + if (hasKnownUrl) { + this.setDisabled(false); + } + } + + const { recordUndo } = Blockly.Events; + Blockly.Events.recordUndo = false; + + loadRemote(this) + .then(() => { + Blockly.Events.recordUndo = recordUndo; + globalObserver.emit('ui.log.success', translate('Blocks are loaded successfully')); + }) + .catch(errorMsg => { + Blockly.Events.recordUndo = recordUndo; + globalObserver.emit('ui.log.error', errorMsg); + }); + + this.currentUrl = this.getFieldValue('URL'); + }, + onchange(event) { + if (event.type === Blockly.Events.BLOCK_CREATE && event.ids.includes(this.id)) { + this.currentUrl = this.getFieldValue('URL'); + this.workspace.getAllBlocks().forEach(block => { + if (block.type === 'loader' && block.id !== this.id) { + if (block.currentUrl === this.currentUrl) { + this.setDisabled(true); + } + } + }); + } + }, +}; + +Blockly.JavaScript.loader = block => { + if (block.loadedVariables.length) { + // eslint-disable-next-line no-underscore-dangle + return `var ${block.loadedVariables.map(v => Blockly.JavaScript.variableDB_.safeName_(v)).toString()}`; + } + return ''; +}; diff --git a/src/scratch/blocks/Binary/Tools/Misc./notify.js b/src/scratch/blocks/Binary/Tools/Misc./notify.js new file mode 100755 index 00000000..97c3df09 --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Misc./notify.js @@ -0,0 +1,42 @@ +import config from '../../../../../constants/const'; +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.notify = { + init() { + this.jsonInit({ + message0: translate('Notify %1 with sound: %2 %3'), + args0 : [ + { + type : 'field_dropdown', + name : 'NOTIFICATION_TYPE', + options: config.lists.NOTIFICATION_TYPE, + }, + { + type : 'field_dropdown', + name : 'NOTIFICATION_SOUND', + options: config.lists.NOTIFICATION_SOUND, + }, + { + type : 'input_value', + name : 'MESSAGE', + check: null, + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + tooltip : translate('Creates a notification'), + }); + }, +}; + +Blockly.JavaScript.notify = block => { + const notificationType = block.getFieldValue('NOTIFICATION_TYPE'); + const sound = block.getFieldValue('NOTIFICATION_SOUND'); + const message = Blockly.JavaScript.valueToCode(block, 'MESSAGE') || `"${translate('')}"`; + + const code = `Bot.notify({ className: '${notificationType}', message: ${message}, sound: '${sound}'});\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Tools/Misc./total_profit.js b/src/scratch/blocks/Binary/Tools/Misc./total_profit.js new file mode 100755 index 00000000..97c3a8f1 --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Misc./total_profit.js @@ -0,0 +1,17 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.total_profit = { + init() { + this.jsonInit({ + message0 : translate('Total Profit'), + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Returns the total profit'), + }); + }, +}; + +Blockly.JavaScript.total_profit = () => ['Bot.getTotalProfit()', Blockly.JavaScript.ORDER_ATOMIC]; diff --git a/src/scratch/blocks/Binary/Tools/Misc./total_runs.js b/src/scratch/blocks/Binary/Tools/Misc./total_runs.js new file mode 100755 index 00000000..46baca5c --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Misc./total_runs.js @@ -0,0 +1,17 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.total_runs = { + init() { + this.jsonInit({ + message0 : translate('Number of Runs'), + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Returns the number of runs since the beginning'), + }); + }, +}; + +Blockly.JavaScript.total_runs = () => ['Bot.getTotalRuns()', Blockly.JavaScript.ORDER_ATOMIC]; diff --git a/src/scratch/blocks/Binary/Tools/Time/epoch.js b/src/scratch/blocks/Binary/Tools/Time/epoch.js new file mode 100755 index 00000000..c7c7655d --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Time/epoch.js @@ -0,0 +1,17 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.epoch = { + init() { + this.jsonInit({ + message0 : translate('Seconds Since Epoch'), + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Returns the epoch time (seconds since epoch)'), + }); + }, +}; + +Blockly.JavaScript.epoch = () => ['Bot.getTime()', Blockly.JavaScript.ORDER_ATOMIC]; diff --git a/src/scratch/blocks/Binary/Tools/Time/index.js b/src/scratch/blocks/Binary/Tools/Time/index.js new file mode 100755 index 00000000..ee9672a0 --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Time/index.js @@ -0,0 +1,2 @@ +import './epoch'; +import './timeout'; diff --git a/src/scratch/blocks/Binary/Tools/Time/timeout.js b/src/scratch/blocks/Binary/Tools/Time/timeout.js new file mode 100755 index 00000000..a4a4ac68 --- /dev/null +++ b/src/scratch/blocks/Binary/Tools/Time/timeout.js @@ -0,0 +1,58 @@ +import { translate } from '../../../../../utils/lang/i18n'; + +Blockly.Blocks.timeout = { + init() { + this.jsonInit({ + message0: translate('%1 %2 Run after %3 second(s)'), + args0 : [ + { + type: 'input_dummy', + }, + { + type: 'input_statement', + name: 'TIMEOUTSTACK', + }, + { + type: 'input_value', + name: 'SECONDS', + }, + ], + colour : '#fef1cf', + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + tooltip : translate('Run the blocks inside every n seconds'), + }); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + const allowedScopes = [ + 'trade_definition', + 'during_purchase', + 'before_purchase', + 'after_purchase', + 'tick_analysis', + ]; + if (allowedScopes.some(scope => this.isDescendantOf(scope))) { + if (this.disabled) { + this.setDisabled(false); + } + } else if (!this.disabled) { + this.setDisabled(true); + } + } + }, +}; + +Blockly.JavaScript.timeout = block => { + const stack = Blockly.JavaScript.statementToCode(block, 'TIMEOUTSTACK'); + const seconds = Blockly.JavaScript.valueToCode(block, 'SECONDS', Blockly.JavaScript.ORDER_ATOMIC) || '1'; + + const code = `sleep(${seconds});\n${stack}\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Trade Definition/index.js b/src/scratch/blocks/Binary/Trade Definition/index.js new file mode 100755 index 00000000..247898e8 --- /dev/null +++ b/src/scratch/blocks/Binary/Trade Definition/index.js @@ -0,0 +1,8 @@ +import './trade_definition'; +import './trade_definition_market'; +import './trade_definition_tradetype'; +import './trade_definition_contracttype'; +import './trade_definition_candleinterval'; +import './trade_definition_restartbuysell'; +import './trade_definition_restartonerror'; +import './trade_definition_tradeoptions'; diff --git a/src/scratch/blocks/Binary/Trade Definition/trade_definition.js b/src/scratch/blocks/Binary/Trade Definition/trade_definition.js new file mode 100755 index 00000000..9475d722 --- /dev/null +++ b/src/scratch/blocks/Binary/Trade Definition/trade_definition.js @@ -0,0 +1,156 @@ +import { defineContract } from '../../images'; +import { setBlockTextColor } from '../../../utils'; +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.trade_definition = { + init() { + this.jsonInit({ + message0: translate('%1 (1) Define your trade contract %2'), + message1: '%1', + message2: translate('Run Once at Start: %1'), + message3: '%1', + message4: translate('Define Trade Options: %1'), + message5: '%1', + args0 : [ + { + type : 'field_image', + src : defineContract, + width : 25, + height: 25, + alt : 'T', + }, + { + type: 'input_dummy', + }, + ], + args1: [ + { + type: 'input_statement', + name: 'TRADE_OPTIONS', + }, + ], + args2: [ + { + type: 'input_dummy', + }, + ], + args3: [ + { + type : 'input_statement', + name : 'INITIALIZATION', + check: null, + }, + ], + args4: [ + { + type: 'input_dummy', + }, + ], + args5: [ + { + type: 'input_statement', + name: 'SUBMARKET', + }, + ], + colour : '#2a3052', + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, + onchange(event) { + setBlockTextColor(this); + if (!this.workspace || this.isInFlyout) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + this.enforceTradeDefinitionType(); + } else if (event.type === Blockly.Events.BLOCK_CREATE) { + if (event.ids && event.ids.includes(this.id)) { + // Maintain single instance of this block + this.workspace.getAllBlocks(true).forEach(block => { + if (block.type === this.type && block.id !== this.id) { + block.dispose(); + } + }); + + const tradeDefinitionMarket = this.getChildByType('trade_definition_market'); + if (!tradeDefinitionMarket) { + return; + } + + const selectedMarket = tradeDefinitionMarket.getFieldValue('MARKET_LIST'); + const eventArgs = [tradeDefinitionMarket, 'field', 'MARKET_LIST', '', selectedMarket]; + const changeEvent = new Blockly.Events.BlockChange(...eventArgs); + Blockly.Events.fire(changeEvent); + } + } + }, + // Check if blocks within statement are valid, we enforce + // this statement to only allow `trade_definition` type blocks. + enforceTradeDefinitionType() { + const blocksInStatement = this.getBlocksInStatement('TRADE_OPTIONS'); + blocksInStatement.forEach(block => { + if (!/^trade_definition_.+$/.test(block.type)) { + Blockly.Events.disable(); + block.unplug(false); + Blockly.Events.enable(); + } + }); + }, + requiredParamBlocks: [ + 'trade_definition_market', + 'trade_definition_tradetype', + 'trade_definition_contracttype', + 'trade_definition_candleinterval', + 'trade_definition_restartbuysell', + 'trade_definition_restartonerror', + ], +}; + +Blockly.JavaScript.trade_definition = block => { + const account = $('.account-id') + .first() + .attr('value'); + if (!account) { + throw Error('Please login'); + } + + const symbol = block.getChildFieldValue('trade_definition_market', 'SYMBOL_LIST') || ''; + const tradeType = block.getChildFieldValue('trade_definition_tradetype', 'TRADETYPE_LIST') || ''; + + // Contract Type (not referring the block) + const contractTypeBlock = block.getChildByType('trade_definition_contracttype'); + const contractTypeSelector = contractTypeBlock.getFieldValue('TYPE_LIST'); + const oppositesName = tradeType.toUpperCase(); + const contractTypeList = + contractTypeSelector === 'both' + ? config.opposites[oppositesName].map(k => Object.keys(k)[0]) + : [contractTypeSelector]; + + const candleIntervalValue = + block.getChildFieldValue('trade_definition_candleinterval', 'CANDLEINTERVAL_LIST') || 'default'; + const shouldRestartOnError = block.childValueToCode('trade_definition_restartonerror', 'RESTARTONERROR') || 'FALSE'; + const timeMachineEnabled = + block.childValueToCode('trade_definition_restartbuysell', 'TIME_MACHINE_ENABLED') || 'FALSE'; + + const initialization = Blockly.JavaScript.statementToCode(block, 'INITIALIZATION'); + const tradeOptionsStatement = Blockly.JavaScript.statementToCode(block, 'SUBMARKET'); + + const code = ` + BinaryBotPrivateInit = function BinaryBotPrivateInit() { + Bot.init('${account}', { + symbol: '${symbol}', + contractTypes: ${JSON.stringify(contractTypeList)}, + candleInterval: '${candleIntervalValue}', + shouldRestartOnError: ${shouldRestartOnError}, + timeMachineEnabled: ${timeMachineEnabled}, + }); + ${initialization.trim()} + }; + BinaryBotPrivateStart = function BinaryBotPrivateStart() { + ${tradeOptionsStatement.trim()} + };\n`; + return code; +}; diff --git a/src/scratch/blocks/Binary/Trade Definition/trade_definition_candleinterval.js b/src/scratch/blocks/Binary/Trade Definition/trade_definition_candleinterval.js new file mode 100755 index 00000000..09f3dc22 --- /dev/null +++ b/src/scratch/blocks/Binary/Trade Definition/trade_definition_candleinterval.js @@ -0,0 +1,35 @@ +import config from '../../../../constants/const'; + +Blockly.Blocks.trade_definition_candleinterval = { + init() { + this.jsonInit({ + message0: 'Default Candle Interval: %1', + args0 : [ + { + type : 'field_dropdown', + name : 'CANDLEINTERVAL_LIST', + options: config.candleIntervals.slice(1), + }, + ], + colour : Blockly.Colours.BinaryLessPurple.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessPurple.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + this.setMovable(false); + this.setDeletable(false); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + this.enforceParent(); + } + }, + enforceParent: Blockly.Blocks.trade_definition_market.enforceParent, +}; +Blockly.JavaScript.trade_definition_candleinterval = () => {}; diff --git a/src/scratch/blocks/Binary/Trade Definition/trade_definition_contracttype.js b/src/scratch/blocks/Binary/Trade Definition/trade_definition_contracttype.js new file mode 100755 index 00000000..a86d30d8 --- /dev/null +++ b/src/scratch/blocks/Binary/Trade Definition/trade_definition_contracttype.js @@ -0,0 +1,67 @@ +import { oppositesToDropdown } from '../../../utils'; +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.trade_definition_contracttype = { + init() { + this.jsonInit({ + message0: 'Contract Type: %1', + args0 : [ + { + type : 'field_dropdown', + name : 'TYPE_LIST', + options: [['', '']], + }, + ], + colour : Blockly.Colours.BinaryLessPurple.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessPurple.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + this.setMovable(false); + this.setDeletable(false); + }, + onchange(event) { + const allowedEvents = [Blockly.Events.BLOCK_CREATE, Blockly.Events.BLOCK_CHANGE, Blockly.Events.END_DRAG]; + if (!this.workspace || this.isInFlyout || !allowedEvents.includes(event.type) || this.workspace.isDragging()) { + return; + } + + const topParentBlock = this.getTopParent(); + if (!topParentBlock || topParentBlock.type !== 'trade_definition') { + this.enforceParent(); + return; + } + + const getContractTypes = () => { + const tradeTypeBlock = topParentBlock.getChildByType('trade_definition_tradetype'); + const tradeType = tradeTypeBlock && tradeTypeBlock.getFieldValue('TRADETYPE_LIST'); + if (tradeType) { + return [[translate('Both'), 'both'], ...oppositesToDropdown(config.opposites[tradeType.toUpperCase()])]; + } + return [['', '']]; + }; + + const updateTypeList = (useDefault = true) => { + const typeList = this.getField('TYPE_LIST'); + const typeListArgs = [getContractTypes()]; + if (useDefault) { + typeListArgs.push(typeList.getValue()); + } + typeList.updateOptions(...typeListArgs); + }; + + if (event.type === Blockly.Events.BLOCK_CHANGE) { + if (event.name === 'TRADETYPE_LIST') { + updateTypeList(); + } + } else if (event.type === Blockly.Events.BLOCK_CREATE) { + if (event.ids.includes(this.id)) { + updateTypeList(); + } + } + }, + enforceParent: Blockly.Blocks.trade_definition_market.enforceParent, +}; +Blockly.JavaScript.trade_definition_contracttype = () => ''; diff --git a/src/scratch/blocks/Binary/Trade Definition/trade_definition_market.js b/src/scratch/blocks/Binary/Trade Definition/trade_definition_market.js new file mode 100755 index 00000000..0eb4a367 --- /dev/null +++ b/src/scratch/blocks/Binary/Trade Definition/trade_definition_market.js @@ -0,0 +1,92 @@ +import { fieldGeneratorMapping } from '../../../shared'; +import { observer as globalObserver } from '../../../../utils/observer'; + +Blockly.Blocks.trade_definition_market = { + init() { + this.jsonInit({ + message0: 'Market: %1 Submarket: %2 Symbol: %3', + args0 : [ + { + type : 'field_dropdown', + name : 'MARKET_LIST', + options: fieldGeneratorMapping.MARKET_LIST, + }, + { + type : 'field_dropdown', + name : 'SUBMARKET_LIST', + options: [['', '']], + }, + { + type : 'field_dropdown', + name : 'SYMBOL_LIST', + options: [['', '']], + }, + ], + colour : Blockly.Colours.BinaryLessPurple.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessPurple.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + this.setMovable(false); + this.setDeletable(false); + }, + onchange(event) { + const allowedEvents = [Blockly.Events.BLOCK_CREATE, Blockly.Events.BLOCK_CHANGE, Blockly.Events.END_DRAG]; + if (!this.workspace || this.isInFlyout || !allowedEvents.includes(event.type) || this.workspace.isDragging()) { + return; + } + + globalObserver.emit('bot.init', this.getFieldValue('SYMBOL_LIST')); + + const topParentBlock = this.getTopParent(); + if (!topParentBlock || topParentBlock.type !== 'trade_definition') { + this.enforceParent(); + return; + } + + const updateMarketLists = (fields, useDefault = true) => { + fields.forEach(field => { + const list = this.getField(field); + const listArgs = [fieldGeneratorMapping[field](this)()]; + if (useDefault) { + listArgs.push(list.getValue()); + } + list.updateOptions(...listArgs); + }); + }; + + if (event.type === Blockly.Events.BLOCK_CREATE) { + if (event.ids.includes(this.id)) { + updateMarketLists(['SUBMARKET_LIST', 'SYMBOL_LIST']); + } + } else if (event.type === Blockly.Events.BLOCK_CHANGE) { + if (event.blockId === this.id) { + if (event.name === 'MARKET_LIST') { + updateMarketLists(['SUBMARKET_LIST']); + } else if (event.name === 'SUBMARKET_LIST') { + updateMarketLists(['SYMBOL_LIST']); + } + } + } + }, + enforceParent() { + if (!this.isDescendantOf('trade_definition')) { + Blockly.Events.disable(); + this.unplug(false); + + const tradeDefinitionBlock = this.workspace.getAllBlocks().find(block => block.type === 'trade_definition'); + if (tradeDefinitionBlock) { + const connection = tradeDefinitionBlock.getLastConnectionInStatement('TRADE_OPTIONS'); + connection.connect(this.previousConnection); + } else { + this.dispose(); + } + + Blockly.Events.enable(); + } + }, +}; + +Blockly.JavaScript.trade_definition_market = () => {}; diff --git a/src/scratch/blocks/Binary/Trade Definition/trade_definition_restartbuysell.js b/src/scratch/blocks/Binary/Trade Definition/trade_definition_restartbuysell.js new file mode 100755 index 00000000..0b094269 --- /dev/null +++ b/src/scratch/blocks/Binary/Trade Definition/trade_definition_restartbuysell.js @@ -0,0 +1,34 @@ +Blockly.Blocks.trade_definition_restartbuysell = { + init() { + this.jsonInit({ + message0: 'Restart buy/sell on error (disable for better performance): %1', + args0 : [ + { + type : 'input_value', + name : 'TIME_MACHINE_ENABLED', + check: 'Boolean', + }, + ], + colour : Blockly.Colours.BinaryLessPurple.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessPurple.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + this.setMovable(false); + this.setDeletable(false); + }, + onchange(event) { + const allowedEvents = [Blockly.Events.BLOCK_CREATE, Blockly.Events.BLOCK_CHANGE, Blockly.Events.END_DRAG]; + if (!this.workspace || this.isInFlyout || !allowedEvents.includes(event.type) || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + this.enforceParent(); + } + }, + enforceParent: Blockly.Blocks.trade_definition_market.enforceParent, +}; +Blockly.JavaScript.trade_definition_restartbuysell = () => {}; diff --git a/src/scratch/blocks/Binary/Trade Definition/trade_definition_restartonerror.js b/src/scratch/blocks/Binary/Trade Definition/trade_definition_restartonerror.js new file mode 100755 index 00000000..1ad12358 --- /dev/null +++ b/src/scratch/blocks/Binary/Trade Definition/trade_definition_restartonerror.js @@ -0,0 +1,34 @@ +Blockly.Blocks.trade_definition_restartonerror = { + init() { + this.jsonInit({ + message0: 'Restart last trade on error (bot ignores the unsuccessful trade): %1', + args0 : [ + { + type : 'input_value', + name : 'RESTARTONERROR', + check: 'Boolean', + }, + ], + colour : Blockly.Colours.BinaryLessPurple.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessPurple.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + this.setMovable(false); + this.setDeletable(false); + }, + onchange(event) { + const allowedEvents = [Blockly.Events.BLOCK_CREATE, Blockly.Events.BLOCK_CHANGE, Blockly.Events.END_DRAG]; + if (!this.workspace || this.isInFlyout || !allowedEvents.includes(event.type) || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + this.enforceParent(); + } + }, + enforceParent: Blockly.Blocks.trade_definition_market.enforceParent, +}; +Blockly.JavaScript.trade_definition_restartonerror = () => {}; diff --git a/src/scratch/blocks/Binary/Trade Definition/trade_definition_tradeoptions.js b/src/scratch/blocks/Binary/Trade Definition/trade_definition_tradeoptions.js new file mode 100755 index 00000000..0d030019 --- /dev/null +++ b/src/scratch/blocks/Binary/Trade Definition/trade_definition_tradeoptions.js @@ -0,0 +1,282 @@ +import { + getBarriersForContracts, + pollForContracts, + getDurationsForContracts, +} from '../../../shared'; +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.trade_definition_tradeoptions = { + init() { + this.jsonInit({ + message0: translate('Duration: %1 %2 Stake: %3 %4'), + args0 : [ + { + type : 'field_dropdown', + name : 'DURATIONTYPE_LIST', + options: [['', '']], + }, + { + type: 'input_value', + name: 'DURATION', + }, + { + type : 'field_dropdown', + name : 'CURRENCY_LIST', + options: config.lists.CURRENCY, + }, + { + type : 'input_value', + name : 'AMOUNT', + check: 'Number', + }, + ], + colour : Blockly.Colours.BinaryLessPurple.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessPurple.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + // Ensure one of this type per statement-stack + this.setNextStatement(false); + }, + onchange(event) { + const allowedEvents = [Blockly.Events.BLOCK_CREATE, Blockly.Events.BLOCK_CHANGE, Blockly.Events.END_DRAG]; + if (!this.workspace || this.isInFlyout || !allowedEvents.includes(event.type) || this.workspace.isDragging()) { + return; + } + + const topParentBlock = this.getTopParent(); + if (!topParentBlock || topParentBlock.type !== 'trade_definition') { + if (!this.disabled) { + this.setDisabled(true); + } + return; + } else if (this.disabled) { + this.setDisabled(false); + } + + const symbol = topParentBlock.getChildFieldValue('trade_definition_market', 'SYMBOL_LIST') || ''; + const tradeTypeBlock = topParentBlock.getChildByType('trade_definition_tradetype'); + const tradeType = tradeTypeBlock.getFieldValue('TRADETYPE_LIST') || ''; + const durationUnit = this.getFieldValue('DURATIONTYPE_LIST'); + + if (event.type === Blockly.Events.BLOCK_CREATE) { + if (event.ids.includes(this.id)) { + pollForContracts(symbol).then(contracts => { + const durations = getDurationsForContracts(contracts, tradeType); + this.updateDurationInput(durations, true); + }); + } + } else if (event.type === Blockly.Events.BLOCK_CHANGE) { + if (event.blockId === this.id) { + if (event.name === 'DURATIONTYPE_LIST') { + pollForContracts(symbol).then(contracts => { + this.updateBarrierInputs(contracts, tradeType, durationUnit); + }); + } else if (event.name === 'BARRIERTYPE_LIST' || event.name === 'SECONDBARRIERTYPE_LIST') { + this.applyBarrierHandlebars(event.name); + pollForContracts(symbol).then(contracts => { + this.updateBarrierInputs(contracts, tradeType, durationUnit); + }); + } + } + } else if (event.type === Blockly.Events.END_DRAG) { + if (event.blockId === this.id) { + pollForContracts(symbol).then(contracts => { + const durations = getDurationsForContracts(contracts, tradeType); + this.updateDurationInput(durations, true); + }); + } + } + }, + createPredictionInput(predictionRange) { + Blockly.Events.disable(); + + if (this.getInput('PREDICTION')) { + return; + } + + this.appendDummyInput('PREDICTION_LABEL').appendField(translate('Prediction:')); + + const predictionInput = this.appendValueInput('PREDICTION'); + + // We can't determine which contract a user buys, so sometimes the prediction range + // returned may not be valid, start at minimum index 1 (if possible) to bypass that + const index = Math.min(1, predictionRange.length - 1); + const shadowBlock = this.workspace.newBlock('math_number'); + + shadowBlock.setShadow(true); + shadowBlock.setFieldValue(predictionRange[index], 'NUM'); + shadowBlock.outputConnection.connect(predictionInput.connection); + shadowBlock.initSvg(); + shadowBlock.render(true); + + Blockly.Events.enable(); + }, + createBarrierInput(barriers, startIndex = 0) { + const inputNames = ['BARRIER', 'SECONDBARRIER']; + const inputLabels = [translate('High barrier'), translate('Low barrier')]; + + for (let i = startIndex; i < barriers.values.length; i++) { + if (this.getInput(inputNames[i])) { + return; + } + + const label = barriers.values.length === 1 ? translate('Barrier') : inputLabels[i]; + const input = this.appendValueInput(inputNames[i]) + .appendField(translate(label), `${inputNames[i]}_LABEL`) + .appendField(new Blockly.FieldDropdown(config.barrierTypes), `${inputNames[i]}TYPE_LIST`); + + const shadowBlock = this.workspace.newBlock('math_number'); + shadowBlock.setShadow(true); + shadowBlock.setFieldValue(barriers.values[i], 'NUM'); + shadowBlock.outputConnection.connect(input.connection); + shadowBlock.initSvg(); + shadowBlock.render(true); + } + }, + updateDurationInput(durations, setMinDuration) { + const durationList = this.getField('DURATIONTYPE_LIST'); + durationList.updateOptions(durations.map(duration => [duration.label, duration.unit])); + + const durationInput = this.getInput('DURATION'); + if (durationInput.connection.isConnected()) { + const targetBlock = durationInput.connection.targetBlock(); + if (targetBlock.isShadow()) { + const minDuration = durations.find(duration => duration.unit === durationList.getValue()); + if (setMinDuration) { + targetBlock.setFieldValue(minDuration.minimum, 'NUM'); + } + } + } + }, + // Only updates a single `trade_definition_tradeoptions` block, i.e. `this` instance + updateBarrierInputs(contracts, tradeType, durationUnit) { + const selectedBarrierTypes = [ + this.getFieldValue('BARRIERTYPE_LIST') || config.barrierTypes[0][1], + this.getFieldValue('SECONDBARRIERTYPE_LIST') || config.barrierTypes[1][1], + ]; + + const barriers = getBarriersForContracts(contracts, tradeType, durationUnit, selectedBarrierTypes); + const inputNames = ['BARRIER', 'SECONDBARRIER']; + + for (let i = 0; i < barriers.values.length; i++) { + const input = this.getInput(inputNames[i]); + + if (input && input.connection.isConnected()) { + const targetBlock = input.connection.targetBlock(); + const barrierTypeList = this.getField(`${inputNames[i]}TYPE_LIST`); + const absoluteType = [[translate('Absolute'), 'absolute']]; + + if (durationUnit === 'd') { + barrierTypeList.updateOptions(absoluteType, 'absolute'); + } else if (barriers.allowBothTypes || barriers.allowAbsoluteType) { + barrierTypeList.updateOptions( + [...config.barrierTypes, ...absoluteType], + barrierTypeList.getValue() + ); + } else { + barrierTypeList.updateOptions(config.barrierTypes, barrierTypeList.getValue()); + } + + if (targetBlock.isShadow()) { + targetBlock.setFieldValue(barriers.values[i], 'NUM'); + } + } + } + }, + // Allow only one type of barrier (i.e. either both offset or absolute barrier type) + applyBarrierHandlebars(barrierInputName) { + const newValue = this.getFieldValue(barrierInputName); + const otherBarrierListName = + barrierInputName === 'BARRIERTYPE_LIST' ? 'SECONDBARRIERTYPE_LIST' : 'BARRIERTYPE_LIST'; + const otherBarrierList = this.getField(otherBarrierListName); + + if (otherBarrierList) { + const otherBarrierType = otherBarrierList.getValue(); + + if (config.barrierTypes.findIndex(type => type[1] === newValue) !== -1 && otherBarrierType === 'absolute') { + const otherValue = config.barrierTypes.find(type => type[1] !== newValue); + + otherBarrierList.setValue(otherValue[1]); + } else if (newValue === 'absolute' && otherBarrierType !== 'absolute') { + otherBarrierList.setValue('absolute'); + } + } + }, + // Rebuild block from XML + domToMutation(xmlElement) { + const hasFirstBarrier = xmlElement.getAttribute('has_first_barrier') === 'true'; + const hasSecondBarrier = xmlElement.getAttribute('has_second_barrier') === 'true'; + const hasPrediction = xmlElement.getAttribute('has_prediction') === 'true'; + + if (hasFirstBarrier && hasSecondBarrier) { + this.createBarrierInput({ values: [1, 2] }); + } else if (hasFirstBarrier) { + this.createBarrierInput({ values: [1] }); + } else if (hasPrediction) { + this.createPredictionInput([1]); + } + }, + // Export mutations to XML + mutationToDom() { + const container = document.createElement('mutation'); + container.setAttribute('has_first_barrier', !!this.getInput('BARRIER')); + container.setAttribute('has_second_barrier', !!this.getInput('SECONDBARRIER')); + container.setAttribute('has_prediction', !!this.getInput('PREDICTION')); + return container; + }, + enforceParent: Blockly.Blocks.trade_definition_market.enforceParent, +}; + +Blockly.JavaScript.trade_definition_tradeoptions = block => { + const durationValue = Blockly.JavaScript.valueToCode(block, 'DURATION') || '0'; + const durationType = block.getFieldValue('DURATIONTYPE_LIST') || '0'; + const currency = block.getFieldValue('CURRENCY_LIST'); + const amount = Blockly.JavaScript.valueToCode(block, 'AMOUNT') || '0'; + + let predictionValue = 'undefined'; + + if (block.getInput('PREDICTION')) { + predictionValue = Blockly.JavaScript.valueToCode(block, 'PREDICTION') || '-1'; + } + + const getBarrierValue = (barrierOffsetType, value) => { + // Variables should not be encapsulated in quotes + if (/^(\d+(\.\d+)?)$/.test(value)) { + return barrierOffsetType === 'absolute' ? `'${value}'` : `'${barrierOffsetType}${value}'`; + } + return barrierOffsetType === 'absolute' ? value : `'${barrierOffsetType}' + ${value}`; + }; + + let barrierOffsetValue = 'undefined'; + let secondBarrierOffsetValue = 'undefined'; + + if (block.getInput('BARRIER')) { + const barrierOffsetType = block.getFieldValue('BARRIERTYPE_LIST'); + const value = Blockly.JavaScript.valueToCode(block, 'BARRIER') || '0'; + barrierOffsetValue = getBarrierValue(barrierOffsetType, value); + } + + if (block.getInput('SECONDBARRIER')) { + const barrierOffsetType = block.getFieldValue('SECONDBARRIERTYPE_LIST'); + const value = Blockly.JavaScript.valueToCode(block, 'SECONDBARRIER') || '0'; + secondBarrierOffsetValue = getBarrierValue(barrierOffsetType, value); + } + + const code = ` + Bot.start({ + limitations: BinaryBotPrivateLimitations, + duration: ${durationValue}, + duration_unit: '${durationType}', + currency: '${currency}', + amount: ${amount}, + prediction: ${predictionValue}, + barrierOffset: ${barrierOffsetValue}, + secondBarrierOffset: ${secondBarrierOffsetValue}, + }); + `; + return code; +}; diff --git a/src/scratch/blocks/Binary/Trade Definition/trade_definition_tradetype.js b/src/scratch/blocks/Binary/Trade Definition/trade_definition_tradetype.js new file mode 100755 index 00000000..b05b893e --- /dev/null +++ b/src/scratch/blocks/Binary/Trade Definition/trade_definition_tradetype.js @@ -0,0 +1,218 @@ +import { + fieldGeneratorMapping, + pollForContracts, + getPredictionForContracts, + getBarriersForContracts, + getDurationsForContracts, +} from '../../../shared'; +import config from '../../../../constants/const'; +import { translate } from '../../../../utils/lang/i18n'; + +Blockly.Blocks.trade_definition_tradetype = { + init() { + this.jsonInit({ + message0: translate('Trade Category: %1 Trade Type: %2'), + args0 : [ + { + type : 'field_dropdown', + name : 'TRADETYPECAT_LIST', + options: [['', '']], + }, + { + type : 'field_dropdown', + name : 'TRADETYPE_LIST', + options: [['', '']], + }, + ], + colour : Blockly.Colours.BinaryLessPurple.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessPurple.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + this.setMovable(false); + this.setDeletable(false); + }, + onchange(event) { + const allowedEvents = [Blockly.Events.BLOCK_CREATE, Blockly.Events.BLOCK_CHANGE, Blockly.Events.END_DRAG]; + if (!this.workspace || this.isInFlyout || !allowedEvents.includes(event.type) || this.workspace.isDragging()) { + return; + } + + const topParentBlock = this.getTopParent(); + if (!topParentBlock || topParentBlock.type !== 'trade_definition') { + this.enforceParent(); + return; + } + const marketBlock = topParentBlock.getChildByType('trade_definition_market'); + if (!marketBlock) { + return; + } + const symbol = marketBlock.getFieldValue('SYMBOL_LIST'); + if (!symbol) { + return; + } + + const updateTradeTypeCatList = (useDefault = false) => { + const tradeTypeCatList = this.getField('TRADETYPECAT_LIST'); + const tradeTypeCatArgs = [fieldGeneratorMapping.TRADETYPECAT_LIST(marketBlock)()]; + if (useDefault) { + tradeTypeCatArgs.push(tradeTypeCatList.getValue()); + } + tradeTypeCatList.updateOptions(...tradeTypeCatArgs); + }; + + const updateTradeTypeList = (useDefault = false) => { + const tradeTypeList = this.getField('TRADETYPE_LIST'); + const tradeTypeArgs = [fieldGeneratorMapping.TRADETYPE_LIST(this)()]; + if (useDefault) { + tradeTypeArgs.push(tradeTypeList.getValue()); + } + tradeTypeList.updateOptions(...tradeTypeArgs); + }; + + if (event.type === Blockly.Events.BLOCK_CHANGE) { + if (event.name === 'MARKET_LIST' || event.name === 'SUBMARKET_LIST' || event.name === 'SYMBOL_LIST') { + updateTradeTypeCatList(); + } else if (event.name === 'TRADETYPECAT_LIST') { + updateTradeTypeList(); + } else if (event.name === 'TRADETYPE_LIST') { + pollForContracts(symbol).then(contracts => { + this.updatePredictionInputs(contracts); + this.updateBarrierInputs(contracts); + this.updateDurationInputs(contracts); + }); + } + } else if (event.type === Blockly.Events.BLOCK_CREATE) { + if (event.ids.includes(this.id)) { + updateTradeTypeCatList(true); + updateTradeTypeList(true); + pollForContracts(symbol).then(contracts => { + this.updateDurationInputs(contracts); + }); + } + } + }, + updateBarrierInputs(contracts) { + const topParentBlock = this.getTopParent(); + this.workspace + .getAllBlocks() + .filter(block => block.type === 'trade_definition_tradeoptions') + .forEach(tradeOptionsBlock => { + const barrierOffsetNames = ['BARRIER', 'SECONDBARRIER']; + const barrierLabels = [translate('High barrier'), translate('Low barrier')]; + + const tradeType = topParentBlock.getChildFieldValue('trade_definition_tradetype', 'TRADETYPE_LIST'); + const durationUnit = tradeOptionsBlock.getFieldValue('DURATIONTYPE_LIST'); + + const firstBarrierType = + tradeOptionsBlock.getFieldValue('BARRIERTYPE_LIST') || config.barrierTypes[0][1]; + const secondBarrierType = + tradeOptionsBlock.getFieldValue('SECONDBARRIERTYPE_LIST') || config.barrierTypes[1][1]; + const selectedBarrierTypes = [firstBarrierType, secondBarrierType]; + const barriers = getBarriersForContracts(contracts, tradeType, durationUnit, selectedBarrierTypes); + + if (barriers.values.length === 0) { + tradeOptionsBlock.removeInput('BARRIER', true); + tradeOptionsBlock.removeInput('SECONDBARRIER', true); + } else { + Blockly.Events.disable(); + + const firstBarrierInput = tradeOptionsBlock.getInput('BARRIER'); + const secondBarrierInput = tradeOptionsBlock.getInput('SECONDBARRIER'); + + if (barriers.values.length > 0) { + if (!firstBarrierInput) { + tradeOptionsBlock.createBarrierInput(barriers); + } + + if (barriers.values.length === 1 && secondBarrierInput) { + tradeOptionsBlock.removeInput('SECONDBARRIER'); + } else if (barriers.values.length === 2 && !secondBarrierInput) { + tradeOptionsBlock.createBarrierInput(barriers, 1); + // Ensure barrier inputs are displayed together + tradeOptionsBlock.moveInputBefore('BARRIER', 'SECONDBARRIER'); + } + + barriers.values.forEach((barrierValue, index) => { + const typeList = tradeOptionsBlock.getField(`${barrierOffsetNames[index]}TYPE_LIST`); + const typeInput = tradeOptionsBlock.getInput(barrierOffsetNames[index]); + const absoluteType = [[translate('Absolute'), 'absolute']]; + + if (durationUnit === 'd') { + typeList.updateOptions(absoluteType); + } else if (barriers.allowBothTypes || barriers.allowAbsoluteType) { + typeList.updateOptions([...config.barrierTypes, ...absoluteType]); + } else { + typeList.updateOptions(config.barrierTypes); + } + + if (barriers.values.length === 1) { + typeInput.fieldRow[0].setText(`${translate('Barrier')}:`); + } else { + typeInput.fieldRow[0].setText(`${barrierLabels[index]}:`); + } + }); + + // Updates Shadow Block values + const barrierInputArgs = [ + contracts, + tradeType, + tradeOptionsBlock.getFieldValue('DURATIONTYPE_LIST'), + ]; + tradeOptionsBlock.updateBarrierInputs(...barrierInputArgs); + } + + tradeOptionsBlock.initSvg(); + tradeOptionsBlock.render(); + + Blockly.Events.enable(); + } + }); + }, + updatePredictionInputs(contracts) { + const topParentBlock = this.getTopParent(); + const tradeType = topParentBlock.getChildFieldValue('trade_definition_tradetype', 'TRADETYPE_LIST'); + const predictionRange = getPredictionForContracts(contracts, tradeType); + + this.workspace + .getAllBlocks() + .filter(block => block.type === 'trade_definition_tradeoptions') + .forEach(tradeOptionsBlock => { + if (predictionRange.length === 0) { + tradeOptionsBlock.removeInput('PREDICTION_LABEL'); + tradeOptionsBlock.removeInput('PREDICTION'); + } else { + const predictionInput = tradeOptionsBlock.getInput('PREDICTION'); + if (predictionInput) { + // TODO: Set new suggested value from API, i.e. first value in prediction range + } else { + tradeOptionsBlock.createPredictionInput(predictionRange); + } + } + tradeOptionsBlock.initSvg(); + tradeOptionsBlock.render(true); + }); + }, + updateDurationInputs(contracts) { + const topParentBlock = this.getTopParent(); + const tradeType = topParentBlock.getChildFieldValue('trade_definition_tradetype', 'TRADETYPE_LIST'); + const durations = getDurationsForContracts(contracts, tradeType); + + this.workspace + .getAllBlocks() + .filter(block => block.type === 'trade_definition_tradeoptions') + .forEach(tradeOptionsBlock => { + const durationUnits = durations.map(duration => [duration.label, duration.unit]); + const durationList = tradeOptionsBlock.getField('DURATIONTYPE_LIST'); + durationList.updateOptions(durationUnits); + + const minDuration = durations.find(duration => duration.unit === durationList.getValue()); + if (minDuration) { + tradeOptionsBlock.updateDurationInput(durations, minDuration.minimum); + } + }); + }, + enforceParent: Blockly.Blocks.trade_definition_market.enforceParent, +}; +Blockly.JavaScript.trade_definition_tradetype = () => ''; diff --git a/src/scratch/blocks/Logic/controls_if.js b/src/scratch/blocks/Logic/controls_if.js new file mode 100755 index 00000000..8432a7b4 --- /dev/null +++ b/src/scratch/blocks/Logic/controls_if.js @@ -0,0 +1,236 @@ +import { + plusIconDark, + minusIconDark, +} from '../images'; +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.controls_if = { + init() { + this.elseIfCount = 0; + this.elseCount = 0; + + this.jsonInit({ + message0: translate('if %1 then'), + message1: '%1', + args0 : [ + { + type : 'input_value', + name : 'IF0', + check: 'Boolean', + }, + ], + args1: [ + { + type: 'input_statement', + name: 'DO0', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + category : Blockly.Categories.control, + previousStatement: null, + nextStatement : null, + }); + + const addInputIcon = this.getAddInputIcon(); + this.appendDummyInput('MUTATOR').appendField(addInputIcon); + }, + /** + * Create XML to represent the number of else-if and else inputs. + * @return {Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom() { + if (!this.elseIfCount && !this.elseCount) { + return null; + } + + const container = document.createElement('mutation'); + + if (this.elseIfCount) { + container.setAttribute('elseif', this.elseIfCount); + } + + if (this.elseCount) { + container.setAttribute('else', 1); + } + + return container; + }, + /** + * Parse XML to restore the else-if and else inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation(xmlElement) { + this.elseIfCount = parseInt(xmlElement.getAttribute('elseif')) || 0; + this.elseCount = parseInt(xmlElement.getAttribute('else')) || 0; + + this.updateShape(); + }, + updateShape() { + // Delete everything. + if (this.getInput('ELSE')) { + this.removeInput('ELSE'); + } + + let i = 1; + while (this.getInput(`IF${i}`)) { + this.removeInput(`IF${i}`); + this.removeInput(`DO${i}`); + + i++; + } + + if (this.getInput('MUTATOR')) { + this.removeInput('MUTATOR'); + } + + // Rebuild block + for (let j = 1; j <= this.elseIfCount; j++) { + this.appendDummyInput(`IF_LABEL${j}`).appendField(translate('else if')); + this.appendValueInput(`IF${j}`).setCheck('Boolean'); + this.appendDummyInput(`THEN_LABEL${j}`).appendField(translate('then')); + this.appendDummyInput(`DELETE_ICON${j}`).appendField(this.getRemoveInputIcon(j, false)); + this.appendStatementInput(`DO${j}`); + } + + if (this.elseCount) { + this.appendDummyInput('ELSE_LABEL').appendField(translate('else')); + this.appendDummyInput('DELETE_ELSE').appendField(this.getRemoveInputIcon(this.elseIfCount + 1, true)); + this.appendStatementInput('ELSE'); + } + + this.appendDummyInput('MUTATOR').appendField(this.getAddInputIcon()); + + this.initSvg(); + this.render(); + }, + getAddInputIcon() { + const onAddClick = () => { + if (!this.workspace || this.isInFlyout) { + return; + } + + const newInputNum = this.elseIfCount + 1; + + if (this.elseCount === 0) { + // No `elseif`, just add an `else`-statement + this.appendDummyInput('ELSE_LABEL').appendField(translate('else')); + this.appendDummyInput('DELETE_ELSE').appendField(this.getRemoveInputIcon(newInputNum, true)); + this.appendStatementInput('ELSE'); + + this.elseCount++; + } else { + // We've already got `elseif` + `else`, keep adding more `elseif`'s + this.appendDummyInput(`IF_LABEL${newInputNum}`).appendField(translate('else if')); + this.appendValueInput(`IF${newInputNum}`).setCheck('Boolean'); + this.appendDummyInput(`THEN_LABEL${newInputNum}`).appendField(translate('then')); + this.appendDummyInput(`DELETE_ICON${newInputNum}`).appendField( + this.getRemoveInputIcon(newInputNum, false) + ); + this.appendStatementInput(`DO${newInputNum}`); + + this.elseIfCount++; + } + + // We already have an else, this input needs to be moved to the bottom where it belongs. + if (this.getInput('ELSE')) { + this.moveInputBefore('ELSE_LABEL', null); + this.moveInputBefore('DELETE_ELSE', null); + this.moveInputBefore('ELSE', null); + } + + // Move plus-icon to the bottom + this.moveInputBefore('MUTATOR', null); + + this.initSvg(); + this.render(); + }; + + const fieldImage = new Blockly.FieldImage(plusIconDark, 24, 24, '+', onAddClick); + return fieldImage; + }, + getRemoveInputIcon(index, isElseStack) { + const onRemoveClick = () => { + if (!this.workspace || this.isInFlyout) { + return; + } + + if (isElseStack) { + this.removeInput('ELSE_LABEL'); + this.removeInput('DELETE_ELSE'); + this.removeInput('ELSE'); + this.elseCount = 0; + } else { + // Determine which label it is, has to be done inside this function. + const inputNames = ['IF_LABEL', 'IF', 'THEN_LABEL', 'DELETE_ICON', 'DO']; + + inputNames.forEach(inputName => { + this.removeInput(`${inputName}${index}`); + + // Re-number inputs w/ indexes larger than this one, e.g. when removing `IF5` becomes `IF4` + let i = 1; + let j = 0; + + // e.g. we've removed `IF5`, name of larger input `IF6` should become `IF5` + let largerInput = this.getInput(inputName + (index + i)); + + while (largerInput) { + const newIndex = index + j; + largerInput.name = inputName + newIndex; + + // Re-attach click handler with correct index. + if (inputName === 'DELETE_ICON') { + for (let k = 0; k < largerInput.fieldRow.length; k++) { + const field = largerInput.fieldRow[k]; + field.dispose(); + largerInput.fieldRow.splice(k, 1); + } + + largerInput.appendField(this.getRemoveInputIcon(newIndex, false)); + } + + i++; + j++; + + largerInput = this.getInput(inputName + (index + i)); + } + }); + + this.elseIfCount--; + } + }; + + const fieldImage = new Blockly.FieldImage(minusIconDark, 24, 24, '-', onRemoveClick); + return fieldImage; + }, +}; + +Blockly.JavaScript.controls_if = block => { + // If/elseif/else condition. + let n = 0; + let code = ''; + + do { + const condition = Blockly.JavaScript.valueToCode(block, `IF${n}`, Blockly.JavaScript.ORDER_NONE) || 'false'; + + // i.e. (else)? if { // code } + const keyword = n > 0 ? 'else if' : 'if'; + code += ` + ${keyword} (${condition}) { + ${Blockly.JavaScript.statementToCode(block, `DO${n}`)} + }`; + n++; + } while (block.getInput(`IF${n}`)); + + if (block.getInput('ELSE')) { + code += ` + else { + ${Blockly.JavaScript.statementToCode(block, 'ELSE')} + }`; + } + + return `${code}\n`; +}; diff --git a/src/scratch/blocks/Logic/index.js b/src/scratch/blocks/Logic/index.js new file mode 100755 index 00000000..41abc76e --- /dev/null +++ b/src/scratch/blocks/Logic/index.js @@ -0,0 +1,7 @@ +import './logic_compare'; +import './controls_if'; +import './logic_boolean'; +import './logic_operation'; +import './logic_null'; +import './logic_ternary'; +import './logic_negate'; diff --git a/src/scratch/blocks/Logic/logic_boolean.js b/src/scratch/blocks/Logic/logic_boolean.js new file mode 100755 index 00000000..11914f3c --- /dev/null +++ b/src/scratch/blocks/Logic/logic_boolean.js @@ -0,0 +1,27 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.logic_boolean = { + init() { + this.jsonInit({ + message0: '%1', + args0 : [ + { + type : 'field_dropdown', + name : 'BOOL', + options: [[translate('true'), 'TRUE'], [translate('false'), 'FALSE']], + }, + ], + output : 'Boolean', + outputShape : Blockly.OUTPUT_SHAPE_HEXAGONAL, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + category : Blockly.Categories.operators, + }); + }, +}; + +Blockly.JavaScript.logic_boolean = block => { + const code = block.getFieldValue('BOOL') === 'TRUE' ? 'true' : 'false'; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Logic/logic_compare.js b/src/scratch/blocks/Logic/logic_compare.js new file mode 100755 index 00000000..f6475b51 --- /dev/null +++ b/src/scratch/blocks/Logic/logic_compare.js @@ -0,0 +1,56 @@ +Blockly.Blocks.logic_compare = { + init() { + this.jsonInit({ + message0: '%1 %2 %3', + args0 : [ + { + type: 'input_value', + name: 'A', + }, + { + type : 'field_dropdown', + name : 'OP', + options: [ + ['=', 'EQ'], + ['\u2260', 'NEQ'], + ['\u200F<', 'LT'], + ['\u200F\u2264', 'LTE'], + ['\u200F>', 'GT'], + ['\u200F\u2265', 'GTE'], + ], + }, + { + type: 'input_value', + name: 'B', + }, + ], + output : 'Boolean', + outputShape : Blockly.OUTPUT_SHAPE_HEXAGONAL, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.logic_compare = block => { + const operatorMapping = { + EQ : '==', + NEQ: '!=', + LT : '<', + LTE: '<=', + GT : '>', + GTE: '>=', + }; + + const operator = operatorMapping[block.getFieldValue('OP') || 'EQ']; + const order = ['==', '!='].includes(operator) + ? Blockly.JavaScript.ORDER_EQUALITY + : Blockly.JavaScript.ORDER_RELATIONAL; + + const argument0 = Blockly.JavaScript.valueToCode(block, 'A', order); + const argument1 = Blockly.JavaScript.valueToCode(block, 'B', order); + + const code = `${argument0} ${operator} ${argument1}`; + return [code, order]; +}; diff --git a/src/scratch/blocks/Logic/logic_negate.js b/src/scratch/blocks/Logic/logic_negate.js new file mode 100755 index 00000000..abfc0d2f --- /dev/null +++ b/src/scratch/blocks/Logic/logic_negate.js @@ -0,0 +1,28 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.logic_negate = { + init() { + this.jsonInit({ + message0: translate('not %1'), + args0 : [ + { + type: 'input_value', + name: 'BOOL', + }, + ], + output : 'Boolean', + outputShape : Blockly.OUTPUT_SHAPE_HEXAGONAL, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.logic_negate = block => { + const order = Blockly.JavaScript.ORDER_LOGICAL_NOT; + const argument0 = Blockly.JavaScript.valueToCode(block, 'BOOL', order) || 'true'; + + const code = `!${argument0}`; + return [code, order]; +}; diff --git a/src/scratch/blocks/Logic/logic_null.js b/src/scratch/blocks/Logic/logic_null.js new file mode 100755 index 00000000..1a7fa47f --- /dev/null +++ b/src/scratch/blocks/Logic/logic_null.js @@ -0,0 +1,13 @@ +Blockly.Blocks.logic_null = { + init() { + this.jsonInit({ + message0 : 'null', + output : null, + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; +Blockly.JavaScript.logic_null = () => ['null', Blockly.JavaScript.ORDER_ATOMIC]; diff --git a/src/scratch/blocks/Logic/logic_operation.js b/src/scratch/blocks/Logic/logic_operation.js new file mode 100755 index 00000000..c8f8f33f --- /dev/null +++ b/src/scratch/blocks/Logic/logic_operation.js @@ -0,0 +1,50 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.logic_operation = { + init() { + this.jsonInit({ + message0: '%1 %2 %3', + args0 : [ + { + type: 'input_value', + name: 'A', + }, + { + type : 'field_dropdown', + name : 'OP', + options: [[translate('and'), 'AND'], [translate('or'), 'OR']], + }, + { + type: 'input_value', + name: 'B', + }, + ], + output : 'Boolean', + outputShape : Blockly.OUTPUT_SHAPE_HEXAGONAL, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.logic_operation = block => { + const selectedOperator = block.getFieldValue('OP'); + + let operator, + order; + + if (selectedOperator === 'AND') { + operator = '&&'; + order = Blockly.JavaScript.ORDER_LOGICAL_AND; + } else if (selectedOperator === 'OR') { + operator = '||'; + order = Blockly.JavaScript.ORDER_LOGICAL_OR; + } + + const argument0 = Blockly.JavaScript.valueToCode(block, 'A'); + const argument1 = Blockly.JavaScript.valueToCode(block, 'B'); + + const code = `${argument0} ${operator} ${argument1}`; + return [code, order]; +}; diff --git a/src/scratch/blocks/Logic/logic_ternary.js b/src/scratch/blocks/Logic/logic_ternary.js new file mode 100755 index 00000000..19b89a01 --- /dev/null +++ b/src/scratch/blocks/Logic/logic_ternary.js @@ -0,0 +1,44 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.logic_ternary = { + init() { + this.jsonInit({ + message0: translate('test %1'), + message1: translate('if true %1'), + message2: translate('if false %1'), + args0 : [ + { + type : 'input_value', + name : 'IF', + check: 'Boolean', + }, + ], + args1: [ + { + type: 'input_value', + name: 'THEN', + }, + ], + args2: [ + { + type: 'input_value', + name: 'ELSE', + }, + ], + output : null, + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.logic_ternary = block => { + const valueIf = Blockly.JavaScript.valueToCode(block, 'IF', Blockly.JavaScript.ORDER_CONDITIONAL) || 'false'; + const valueThen = Blockly.JavaScript.valueToCode(block, 'THEN', Blockly.JavaScript.ORDER_CONDITIONAL) || 'null'; + const valueElse = Blockly.JavaScript.valueToCode(block, 'ELSE', Blockly.JavaScript.ORDER_CONDITIONAL) || 'null'; + + const code = `${valueIf} ? ${valueThen} : ${valueElse}`; + return [code, Blockly.JavaScript.ORDER_CONDITIONAL]; +}; diff --git a/src/scratch/blocks/Math/index.js b/src/scratch/blocks/Math/index.js new file mode 100755 index 00000000..2972e703 --- /dev/null +++ b/src/scratch/blocks/Math/index.js @@ -0,0 +1,14 @@ +import './math_change'; +import './math_constant'; +import './math_constrain'; +import './math_modulo'; +import './math_number'; +import './math_number_positive'; +import './math_number_property'; +import './math_on_list'; +import './math_random_float'; +import './math_random_int'; +import './math_round'; +import './math_arithmetic'; +import './math_single'; +import './math_trig'; diff --git a/src/scratch/blocks/Math/math_arithmetic.js b/src/scratch/blocks/Math/math_arithmetic.js new file mode 100755 index 00000000..ad4b0be6 --- /dev/null +++ b/src/scratch/blocks/Math/math_arithmetic.js @@ -0,0 +1,57 @@ +Blockly.Blocks.math_arithmetic = { + init() { + this.jsonInit({ + message0: '%1 %2 %3', + args0 : [ + { + type : 'input_value', + name : 'A', + check: 'Number', + }, + { + type : 'field_dropdown', + name : 'OP', + options: [['+', 'ADD'], ['-', 'MINUS'], ['*', 'MULTIPLY'], ['/', 'DIVIDE'], ['^', 'POWER']], + }, + { + type : 'input_value', + name : 'B', + check: 'Number', + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.math_arithmetic = block => { + const operators = { + ADD : ['+', Blockly.JavaScript.ORDER_ADDITION], + MINUS : ['-', Blockly.JavaScript.ORDER_SUBTRACTION], + MULTIPLY: ['*', Blockly.JavaScript.ORDER_MULTIPLICATION], + DIVIDE : ['/', Blockly.JavaScript.ORDER_DIVISION], + POWER : [null, Blockly.JavaScript.ORDER_COMMA], // Handle power separately. + }; + + const tuple = operators[block.getFieldValue('OP')]; + const operator = tuple[0]; + const order = tuple[1]; + + const argument0 = Blockly.JavaScript.valueToCode(block, 'A', order) || '0'; + const argument1 = Blockly.JavaScript.valueToCode(block, 'B', order) || '0'; + + let code; + + // Power in JavaScript requires a special case since it has no operator. + if (!operator) { + code = `Math.pow(${argument0}, ${argument1})`; + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; + } + + code = `${argument0} ${operator} ${argument1}`; + return [code, order]; +}; diff --git a/src/scratch/blocks/Math/math_change.js b/src/scratch/blocks/Math/math_change.js new file mode 100755 index 00000000..52c3d99d --- /dev/null +++ b/src/scratch/blocks/Math/math_change.js @@ -0,0 +1,40 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.math_change = { + init() { + this.jsonInit({ + message0: translate('change %1 by %2'), + args0 : [ + { + type : 'field_variable', + name : 'VAR', + variable: translate('item'), + }, + { + type : 'input_value', + name : 'DELTA', + check: 'Number', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, +}; + +Blockly.JavaScript.math_change = block => { + const variable = block.getFieldValue('VAR'); + // eslint-disable-next-line no-underscore-dangle + const argument0 = Blockly.JavaScript.variableDB_.getName(variable, Blockly.Variables.NAME_TYPE); + const argument1 = Blockly.JavaScript.valueToCode(block, 'DELTA', Blockly.JavaScript.ORDER_ADDITION) || '0'; + + const code = ` + if (typeof ${argument0} != 'number') { + ${argument0} = 0; + }; + ${argument0} += ${argument1};\n`; + return code; +}; diff --git a/src/scratch/blocks/Math/math_constant.js b/src/scratch/blocks/Math/math_constant.js new file mode 100755 index 00000000..b7d4be83 --- /dev/null +++ b/src/scratch/blocks/Math/math_constant.js @@ -0,0 +1,55 @@ +Blockly.Blocks.math_constant = { + init() { + this.jsonInit({ + message0: '%1', + args0 : [ + { + type : 'field_dropdown', + name : 'CONSTANT', + options: [ + ['\u03C0', 'PI'], + ['\u2107', 'E'], + ['\u03d5', 'GOLDEN_RATIO'], + ['sqrt(2)', 'SQRT2'], + ['sqrt(\u00bd)', 'SQRT1_2'], + ['\u221e', 'INFINITY'], + ], + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.math_constant = block => { + const constant = block.getFieldValue('CONSTANT'); + + let code, + order; + + if (constant === 'PI') { + code = 'Math.PI'; + order = Blockly.JavaScript.ORDER_MEMBER; + } else if (constant === 'E') { + code = 'Math.E'; + order = Blockly.JavaScript.ORDER_MEMBER; + } else if (constant === 'GOLDEN_RATIO') { + code = '(1 + Math.sqrt(5)) / 2'; + order = Blockly.JavaScript.ORDER_DIVISION; + } else if (constant === 'SQRT2') { + code = 'Math.SQRT2'; + order = Blockly.JavaScript.ORDER_MEMBER; + } else if (constant === 'SQRT1_2') { + code = 'Math.SQRT1_2'; + order = Blockly.JavaScript.ORDER_MEMBER; + } else if (constant === 'INFINITY') { + code = 'Infinity'; + order = Blockly.JavaScript.ORDER_ATOMIC; + } + + return [code, order]; +}; diff --git a/src/scratch/blocks/Math/math_constrain.js b/src/scratch/blocks/Math/math_constrain.js new file mode 100755 index 00000000..3d3c6dee --- /dev/null +++ b/src/scratch/blocks/Math/math_constrain.js @@ -0,0 +1,40 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.math_constrain = { + init() { + this.jsonInit({ + message0: translate('constrain %1 low %2 high %3'), + args0 : [ + { + type : 'input_value', + name : 'VALUE', + check: 'Number', + }, + { + type : 'input_value', + name : 'LOW', + check: 'Number', + }, + { + type : 'input_value', + name : 'HIGH', + check: 'Number', + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.math_constrain = block => { + const argument0 = Blockly.JavaScript.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_COMMA) || '0'; + const argument1 = Blockly.JavaScript.valueToCode(block, 'LOW', Blockly.JavaScript.ORDER_COMMA) || '0'; + const argument2 = Blockly.JavaScript.valueToCode(block, 'HIGH', Blockly.JavaScript.ORDER_COMMA) || '0'; + + const code = `Math.min(Math.max(${argument0}, ${argument1}), ${argument2})`; + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Math/math_modulo.js b/src/scratch/blocks/Math/math_modulo.js new file mode 100755 index 00000000..f060bd60 --- /dev/null +++ b/src/scratch/blocks/Math/math_modulo.js @@ -0,0 +1,34 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.math_modulo = { + init() { + this.jsonInit({ + message0: translate('remainder of %1 ÷ %2'), + args0 : [ + { + type : 'input_value', + name : 'DIVIDEND', + check: 'Number', + }, + { + type : 'input_value', + name : 'DIVISOR', + check: 'Number', + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.math_modulo = block => { + const argument0 = Blockly.JavaScript.valueToCode(block, 'DIVIDEND', Blockly.JavaScript.ORDER_MODULUS) || '0'; + const argument1 = Blockly.JavaScript.valueToCode(block, 'DIVISOR', Blockly.JavaScript.ORDER_MODULUS) || '0'; + + const code = `${argument0} % ${argument1}`; + return [code, Blockly.JavaScript.ORDER_MODULUS]; +}; diff --git a/src/scratch/blocks/Math/math_number.js b/src/scratch/blocks/Math/math_number.js new file mode 100755 index 00000000..1110039b --- /dev/null +++ b/src/scratch/blocks/Math/math_number.js @@ -0,0 +1,33 @@ +Blockly.Blocks.math_number = { + init() { + this.jsonInit({ + message0: '%1', + args0 : [ + { + type : 'field_number', + name : 'NUM', + value: 0, + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : '#dedede', + colourSecondary: '#ffffff', + colourTertiary : '#ffffff', + }); + + const fieldInput = this.getField('NUM'); + fieldInput.setValidator(input => this.numberValidator(input)); + }, + numberValidator(input) { + if (/^-?([0][,.]|[1-9]+[,.])?([0]|[1-9])*$/.test(input)) { + return undefined; + } + return null; + }, +}; + +Blockly.JavaScript.math_number = block => { + const code = block.getFieldValue('NUM'); + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Math/math_number_positive.js b/src/scratch/blocks/Math/math_number_positive.js new file mode 100755 index 00000000..bfb77a34 --- /dev/null +++ b/src/scratch/blocks/Math/math_number_positive.js @@ -0,0 +1,14 @@ +Blockly.Blocks.math_number_positive = { + init: Blockly.Blocks.math_number.init, + numberValidator(input) { + if (/^([0][,.]|[1-9]+[,.])?([0]|[1-9])*$/.test(input)) { + return undefined; + } + return null; + }, +}; + +Blockly.JavaScript.math_number_positive = block => { + const code = block.getFieldValue('NUM'); + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Math/math_number_property.js b/src/scratch/blocks/Math/math_number_property.js new file mode 100755 index 00000000..b68f6c02 --- /dev/null +++ b/src/scratch/blocks/Math/math_number_property.js @@ -0,0 +1,114 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.math_number_property = { + init() { + this.jsonInit({ + message0: translate('%1 is %2'), + args0 : [ + { + type: 'input_value', + name: 'NUMBER_TO_CHECK', + }, + { + type : 'field_dropdown', + name : 'PROPERTY', + options: [ + [translate('even'), 'EVEN'], + [translate('odd'), 'ODD'], + [translate('prime'), 'PRIME'], + [translate('whole'), 'WHOLE'], + [translate('positive'), 'POSITIVE'], + [translate('negative'), 'NEGATIVE'], + [translate('divisible by'), 'DIVISIBLE_BY'], + ], + }, + ], + output : 'Boolean', + outputShape : Blockly.OUTPUT_SHAPE_HEXAGONAL, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + + this.setOnChange(event => { + if (event.name === 'PROPERTY') { + const hasDivisorInput = this.getFieldValue('PROPERTY') === 'DIVISIBLE_BY'; + this.updateShape(hasDivisorInput); + } + }); + }, + domToMutation(xmlElement) { + const hasDivisorInput = xmlElement.getAttribute('divisor_input') === 'true'; + this.updateShape(hasDivisorInput); + }, + mutationToDom() { + const container = document.createElement('mutation'); + const divisorInput = this.getFieldValue('PROPERTY') === 'DIVISIBLE_BY'; + container.setAttribute('divisor_input', divisorInput); + return container; + }, + updateShape(hasDivisorInput) { + const inputExists = this.getInput('DIVISOR'); + + if (hasDivisorInput) { + if (!inputExists) { + this.appendValueInput('DIVISOR').setCheck('Number'); + this.initSvg(); + this.render(false); + } + } else { + this.removeInput('DIVISOR'); + } + }, +}; + +Blockly.JavaScript.math_number_property = block => { + const argument0 = Blockly.JavaScript.valueToCode(block, 'NUMBER_TO_CHECK', Blockly.JavaScript.ORDER_MODULUS) || '0'; + const property = block.getFieldValue('PROPERTY'); + + let code; + + if (property === 'PRIME') { + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.provideFunction_('mathIsPrime', [ + // eslint-disable-next-line no-underscore-dangle + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(n) { + // https://en.wikipedia.org/wiki/Primality_test#Naive_methods + if (n == 2 || n == 3) { + return true; + } + + // False if n is NaN, negative, is 1, or not whole. + // And false if n is divisible by 2 or 3. + if (isNaN(n) || n <= 1 || n % 1 != 0 || n % 2 == 0 || n % 3 == 0) { + return false; + } + + // Check all the numbers of form 6k +/- 1, up to sqrt(n). + for (var x = 6; x <= Math.sqrt(n) + 1; x += 6) { + if (n % (x - 1) == 0 || n % (x + 1) == 0) { + return false; + } + } + return true; + }`, + ]); + code = `${functionName}(${argument0})`; + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; + } else if (property === 'EVEN') { + code = `${argument0} % 2 === 0`; + } else if (property === 'ODD') { + code = `${argument0} % 2 === 1`; + } else if (property === 'WHOLE') { + code = `${argument0} % 1 === 0`; + } else if (property === 'POSITIVE') { + code = `${argument0} > 0`; + } else if (property === 'NEGATIVE') { + code = `${argument0} < 0`; + } else if (property === 'DIVISIBLE_BY') { + const divisor = Blockly.JavaScript.valueToCode(block, 'DIVISOR', Blockly.JavaScript.ORDER_MODULUS) || '0'; + code = `${argument0} % ${divisor} == 0`; + } + + return [code, Blockly.JavaScript.ORDER_EQUALITY]; +}; diff --git a/src/scratch/blocks/Math/math_on_list.js b/src/scratch/blocks/Math/math_on_list.js new file mode 100755 index 00000000..4659b937 --- /dev/null +++ b/src/scratch/blocks/Math/math_on_list.js @@ -0,0 +1,160 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.math_on_list = { + init() { + this.jsonInit({ + message0: translate('%1 of list %2'), + args0 : [ + { + type : 'field_dropdown', + name : 'OPERATION', + options: [ + [translate('sum'), 'SUM'], + [translate('min'), 'MIN'], + [translate('max'), 'MAX'], + [translate('average'), 'AVERAGE'], + [translate('median'), 'MEDIAN'], + [translate('modes'), 'MODE'], + [translate('standard deviation'), 'STD_DEV'], + [translate('random item'), 'RANDOM'], + ], + }, + { + type: 'input_value', + name: 'LIST', + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +/* eslint-disable no-underscore-dangle */ +Blockly.JavaScript.math_on_list = block => { + const operation = block.getFieldValue('OPERATION'); + + let code, + list; + + if (operation === 'SUM') { + list = Blockly.JavaScript.valueToCode(block, 'LIST', Blockly.JavaScript.ORDER_MEMBER) || '[]'; + code = `${list}.reduce(function(x, y) { return x + y; })`; + } else if (operation === 'MIN') { + list = Blockly.JavaScript.valueToCode(block, 'LIST', Blockly.JavaScript.ORDER_COMMA) || '[]'; + code = `Math.min.apply(null, ${list})`; + } else if (operation === 'MAX') { + list = Blockly.JavaScript.valueToCode(block, 'LIST', Blockly.JavaScript.ORDER_COMMA) || '[]'; + code = `Math.max.apply(null, ${list})`; + } else if (operation === 'AVERAGE') { + const functionName = Blockly.JavaScript.provideFunction_('mathMean', [ + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(myList) { + return myList.reduce(function(x, y) { + return x + y; + }) / myList.length; + }`, + ]); + + list = Blockly.JavaScript.valueToCode(block, 'LIST', Blockly.JavaScript.ORDER_NONE) || '[]'; + code = `${functionName}(${list})`; + } else if (operation === 'MEDIAN') { + const functionName = Blockly.JavaScript.provideFunction_('mathMedian', [ + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(myList) { + var localList = myList.filter(function(x) { + return typeof x == 'number'; + }); + if (!localList.length) { + return null; + } + localList.sort(function(a, b) { + return b - a; + }); + if (localList.length % 2 == 0) { + return (localList[localList.length / 2 - 1] + localList[localList.length / 2]) / 2; + } else { + return localList[(localList.length - 1) / 2]; + } + }`, + ]); + + list = Blockly.JavaScript.valueToCode(block, 'LIST', Blockly.JavaScript.ORDER_NONE) || '[]'; + code = `${functionName}(${list})`; + } else if (operation === 'MODE') { + const functionName = Blockly.JavaScript.provideFunction_('mathModes', [ + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(values) { + var modes = []; + var counts = []; + var maxCount = 0; + + for (var i = 0; i < values.length; i++) { + var value = values[i]; + var found = false; + var thisCount; + + for (var j = 0; j < counts.length; j++) { + if (counts[j][0] === value) { + thisCount = ++counts[j][1]; + found = true; + break; + } + } + + if (!found) { + counts.push([value, 1]); + thisCount = 1; + } + maxCount = Math.max(thisCount, maxCount); + } + + for (var j = 0; j < counts.length; j++) { + if (counts[j][1] == maxCount) { + modes.push(counts[j][0]); + } + } + + return modes; + }`, + ]); + + list = Blockly.JavaScript.valueToCode(block, 'LIST', Blockly.JavaScript.ORDER_NONE) || '[]'; + code = `${functionName}(${list})`; + } else if (operation === 'STD_DEV') { + const functionName = Blockly.JavaScript.provideFunction_('mathStandardDeviation', [ + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(numbers) { + var n = numbers.length; + if (!n) { + return null; + } + + var mean = numbers.reduce(function(x, y) { + return x + y; + }) / n; + + var variance = 0; + for (var j = 0; j < n; j++) { + variance += Math.pow(numbers[j] - mean, 2); + } + variance = variance / n; + return Math.sqrt(variance); + }`, + ]); + + list = Blockly.JavaScript.valueToCode(block, 'LIST', Blockly.JavaScript.ORDER_NONE) || '[]'; + code = `${functionName}(${list})`; + } else if (operation === 'RANDOM') { + const functionName = Blockly.JavaScript.provideFunction_('mathRandomList', [ + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(list) { + var x = Math.floor(Math.random() * list.length); + return list[x]; + }`, + ]); + + list = Blockly.JavaScript.valueToCode(block, 'LIST', Blockly.JavaScript.ORDER_NONE) || '[]'; + code = `${functionName}(${list})`; + } + + return [code, Blockly.JavaScript.FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Math/math_random_float.js b/src/scratch/blocks/Math/math_random_float.js new file mode 100755 index 00000000..c74d43e4 --- /dev/null +++ b/src/scratch/blocks/Math/math_random_float.js @@ -0,0 +1,16 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.math_random_float = { + init() { + this.jsonInit({ + message0 : translate('random fraction'), + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.math_random_float = () => ['Math.random()', Blockly.JavaScript.ORDER_FUNCTION_CALL]; diff --git a/src/scratch/blocks/Math/math_random_int.js b/src/scratch/blocks/Math/math_random_int.js new file mode 100755 index 00000000..2b843a3b --- /dev/null +++ b/src/scratch/blocks/Math/math_random_int.js @@ -0,0 +1,48 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.math_random_int = { + init() { + this.jsonInit({ + message0: translate('random integer from %1 to %2'), + args0 : [ + { + type : 'input_value', + name : 'FROM', + check: 'Number', + }, + { + type : 'input_value', + name : 'TO', + check: 'Number', + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.math_random_int = block => { + const argument0 = Blockly.JavaScript.valueToCode(block, 'FROM', Blockly.JavaScript.ORDER_COMMA) || '0'; + const argument1 = Blockly.JavaScript.valueToCode(block, 'TO', Blockly.JavaScript.ORDER_COMMA) || '0'; + + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.provideFunction_('mathRandomInt', [ + // eslint-disable-next-line no-underscore-dangle + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(a, b) { + if (a > b) { + // Swap a and b to ensure a is smaller. + var c = a; + a = b; + b = c; + } + return Math.floor(Math.random() * (b - a + 1) + a); + }`, + ]); + + const code = `${functionName}(${argument0}, ${argument1})`; + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Math/math_round.js b/src/scratch/blocks/Math/math_round.js new file mode 100755 index 00000000..4df760b1 --- /dev/null +++ b/src/scratch/blocks/Math/math_round.js @@ -0,0 +1,46 @@ +// https://github.com/google/blockly/blob/master/generators/javascript/math.js +Blockly.Blocks.math_round = { + /** + * Check if a number is even, odd, prime, whole, positive, or negative + * or if it is divisible by certain number. Returns true or false. + * @this Blockly.Block + */ + init() { + this.jsonInit({ + message0: '%1 %2', + args0 : [ + { + type : 'field_dropdown', + name : 'OP', + options: [['round', 'ROUND'], ['round up', 'ROUNDUP'], ['round down', 'ROUNDDOWN']], + }, + { + type: 'input_value', + name: 'NUM', + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.math_round = block => { + const operation = block.getFieldValue('OP'); + const argument0 = Blockly.JavaScript.valueToCode(block, 'NUM') || '0'; + + let code; + + if (operation === 'ROUND') { + code = `Math.round(${argument0})`; + } else if (operation === 'ROUNDUP') { + code = `Math.ceil(${argument0})`; + } else if (operation === 'ROUNDDOWN') { + code = `Math.floor(${argument0})`; + } + + return [code, Blockly.JavaScript.FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Math/math_single.js b/src/scratch/blocks/Math/math_single.js new file mode 100755 index 00000000..df1bc163 --- /dev/null +++ b/src/scratch/blocks/Math/math_single.js @@ -0,0 +1,101 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.math_single = { + init() { + this.jsonInit({ + message0: translate('%1 %2'), + args0 : [ + { + type : 'field_dropdown', + name : 'OP', + options: [ + [translate('square root'), 'ROOT'], + [translate('absolute'), 'ABS'], + ['-', 'NEG'], + ['ln', 'LN'], + ['log10', 'LOG10'], + ['e^', 'EXP'], + ['10^', 'POW10'], + ], + }, + { + type: 'input_value', + name: 'NUM', + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.math_single = block => { + const operator = block.getFieldValue('OP'); + + let code, + arg; + + if (operator === 'NEG') { + // Negation is a special case given its different operator precedence. + arg = Blockly.JavaScript.valueToCode(block, 'NUM', Blockly.JavaScript.ORDER_UNARY_NEGATION) || '0'; + if (arg[0] === '-') { + // --3 is not legal in JS + arg = ` ${arg}`; + } + code = `-${arg}`; + return [code, Blockly.JavaScript.ORDER_UNARY_NEGATION]; + } + + if (['SIN', 'COS', 'TAN'].includes(operator)) { + arg = Blockly.JavaScript.valueToCode(block, 'NUM', Blockly.JavaScript.ORDER_DIVISION) || '0'; + } else { + arg = Blockly.JavaScript.valueToCode(block, 'NUM', Blockly.JavaScript.ORDER_NONE) || '0'; + } + + // First, handle cases which generate values that don't need parentheses + // wrapping the code. + if (operator === 'ABS') { + code = `Math.abs(${arg})`; + } else if (operator === 'ROOT') { + code = `Math.sqrt(${arg})`; + } else if (operator === 'LN') { + code = `Math.log(${arg})`; + } else if (operator === 'EXP') { + code = `Math.pow(Math.E, ${arg})`; + } else if (operator === 'POW10') { + code = `Math.pow(10, ${arg})`; + } else if (operator === 'ROUND') { + code = `Math.round(${arg})`; + } else if (operator === 'ROUNDUP') { + code = `Math.ceil(${arg})`; + } else if (operator === 'ROUNDDOWN') { + code = `Math.floor(${arg})`; + } else if (operator === 'SIN') { + code = `Math.sin(${arg} / 180 * Math.PI)`; + } else if (operator === 'COS') { + code = `Math.cos(${arg} / 180 * Math.PI)`; + } else if (operator === 'TAN') { + code = `Math.tan(${arg} / 180 * Math.PI)`; + } + + if (code) { + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; + } + + // Second, handle cases which generate values that may need parentheses + // wrapping the code. + if (operator === 'LOG10') { + code = `Math.log(${arg}) / Math.log(10)`; + } else if (operator === 'ASIN') { + code = `Math.asin(${arg}) / Math.PI * 180`; + } else if (operator === 'ACOS') { + code = `Math.acos(${arg}) / Math.PI * 180`; + } else if (operator === 'ATAN') { + code = `Math.atan(${arg}) / Math.PI * 180`; + } + + return [code, Blockly.JavaScript.ORDER_DIVISION]; +}; diff --git a/src/scratch/blocks/Math/math_trig.js b/src/scratch/blocks/Math/math_trig.js new file mode 100755 index 00000000..62ad80f2 --- /dev/null +++ b/src/scratch/blocks/Math/math_trig.js @@ -0,0 +1,35 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.math_trig = { + init() { + this.jsonInit({ + message0: translate('%1 %2'), + args0 : [ + { + type : 'field_dropdown', + name : 'OP', + options: [ + ['sin', 'SIN'], + ['cos', 'COS'], + ['tan', 'TAN'], + ['asin', 'ASIN'], + ['acos', 'ACOS'], + ['atan', 'ATAN'], + ], + }, + { + type : 'input_value', + name : 'NUM', + check: 'Number', + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.math_trig = Blockly.JavaScript.math_single; diff --git a/src/scratch/blocks/Text/index.js b/src/scratch/blocks/Text/index.js new file mode 100755 index 00000000..bbdfbfdb --- /dev/null +++ b/src/scratch/blocks/Text/index.js @@ -0,0 +1,13 @@ +import './text'; +import './text_join'; +import './text_statement'; +import './text_append'; +import './text_length'; +import './text_isEmpty'; +import './text_indexOf'; +import './text_charAt'; +import './text_getSubstring'; +import './text_changeCase'; +import './text_trim'; +import './text_print'; +import './text_prompt_ext'; diff --git a/src/scratch/blocks/Text/text.js b/src/scratch/blocks/Text/text.js new file mode 100755 index 00000000..90cce4b4 --- /dev/null +++ b/src/scratch/blocks/Text/text.js @@ -0,0 +1,24 @@ +Blockly.Blocks.text = { + init() { + this.jsonInit({ + message0: '%1', + args0 : [ + { + type: 'field_input', + name: 'TEXT', + }, + ], + output : 'String', + outputShape : Blockly.OUTPUT_SHAPE_SQUARE, + colour : '#dedede', + colourSecondary: '#ffffff', + colourTertiary : '#ffffff', + }); + }, +}; + +Blockly.JavaScript.text = block => { + // eslint-disable-next-line no-underscore-dangle + const code = Blockly.JavaScript.quote_(block.getFieldValue('TEXT')); + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Text/text_append.js b/src/scratch/blocks/Text/text_append.js new file mode 100755 index 00000000..c01314a0 --- /dev/null +++ b/src/scratch/blocks/Text/text_append.js @@ -0,0 +1,42 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.text_append = { + init() { + this.jsonInit({ + message0: translate('to %1 append text %2'), + args0 : [ + { + type : 'field_variable', + name : 'VAR', + variable: translate('text'), + }, + { + type: 'input_value', + name: 'TEXT', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, +}; + +Blockly.JavaScript.text_append = block => { + const forceString = value => { + const strRegExp = /^\s*'([^']|\\')*'\s*$/; + if (strRegExp.test(value)) { + return value; + } + return `String(${value})`; + }; + + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName(block.getFieldValue('VAR'), Blockly.Variables.NAME_TYPE); + const value = Blockly.JavaScript.valueToCode(block, 'TEXT', Blockly.JavaScript.ORDER_NONE) || '\'\''; + + const code = `${varName} += ${forceString(value)};\n`; + return code; +}; diff --git a/src/scratch/blocks/Text/text_changeCase.js b/src/scratch/blocks/Text/text_changeCase.js new file mode 100755 index 00000000..5d84e5f5 --- /dev/null +++ b/src/scratch/blocks/Text/text_changeCase.js @@ -0,0 +1,59 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.text_changeCase = { + init() { + this.jsonInit({ + message0: translate('to %1 %2'), + args0 : [ + { + type : 'field_dropdown', + name : 'CASE', + options: [ + [translate('UPPER CASE'), 'UPPERCASE'], + [translate('lower case'), 'LOWERCASE'], + [translate('Title Case'), 'TITLECASE'], + ], + }, + { + type: 'input_value', + name: 'TEXT', + }, + ], + output : 'String', + outputShape : Blockly.OUTPUT_SHAPE_SQUARE, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.text_changeCase = block => { + const operators = { + UPPERCASE: '.toUpperCase()', + LOWERCASE: '.toLowerCase()', + TITLECASE: null, + }; + const operator = operators[block.getFieldValue('CASE')]; + const textOrder = operator ? Blockly.JavaScript.ORDER_MEMBER : Blockly.JavaScript.ORDER_NONE; + const text = Blockly.JavaScript.valueToCode(block, 'TEXT', textOrder) || '\'\''; + + let code; + + if (operator) { + code = `${text}${operator}`; + } else { + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.provideFunction_('textToTitleCase', [ + // eslint-disable-next-line no-underscore-dangle + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(str) { + return str.toLowerCase().split(' ').map(function(word) { + return word.replace(word[0], word[0].toUpperCase()); + }).join(' '); + }`, + ]); + code = `${functionName}(${text})`; + } + + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Text/text_charAt.js b/src/scratch/blocks/Text/text_charAt.js new file mode 100755 index 00000000..81e7ef44 --- /dev/null +++ b/src/scratch/blocks/Text/text_charAt.js @@ -0,0 +1,96 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.text_charAt = { + init() { + this.jsonInit({ + message0: translate('in text %1 get %2'), + args0 : [ + { + type: 'input_value', + name: 'VALUE', + }, + { + type : 'field_dropdown', + name : 'WHERE', + options: [ + [translate('letter #'), 'FROM_START'], + [translate('letter # from end'), 'FROM_END'], + [translate('first letter'), 'FIRST'], + [translate('last letter'), 'LAST'], + [translate('random letter'), 'RANDOM'], + ], + }, + ], + output : 'String', + outputShape : Blockly.OUTPUT_SHAPE_SQUARE, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + + const dropdown = this.getField('WHERE'); + dropdown.setValidator(value => { + const newAt = ['FROM_START', 'FROM_END'].includes(value); + if (newAt !== this.isAt) { + this.updateAt(newAt); + this.setFieldValue(value, 'WHERE'); + return null; + } + return undefined; + }); + + this.updateAt(true); + }, + mutationToDom() { + const container = document.createElement('mutation'); + container.setAttribute('at', !!this.isAt); + return container; + }, + domToMutation(xmlElement) { + const isAt = xmlElement.getAttribute('at') !== 'false'; + this.updateAt(isAt); + }, + updateAt(isAt) { + this.removeInput('AT', true); + if (isAt) { + this.appendValueInput('AT').setCheck('Number'); + } + + this.isAt = isAt; + this.initSvg(); + this.render(false); + }, +}; + +Blockly.JavaScript.text_charAt = block => { + const where = block.getFieldValue('WHERE') || 'FROM_START'; + const textOrder = where === 'RANDOM' ? Blockly.JavaScript.ORDER_NONE : Blockly.JavaScript.ORDER_MEMBER; + const text = Blockly.JavaScript.valueToCode(block, 'VALUE', textOrder) || '\'\''; + + let code; + + if (where === 'FROM_START') { + const at = Blockly.JavaScript.getAdjusted(block, 'AT'); + // Adjust index if using one-based indices + code = `${text}.charAt(${at})`; + } else if (where === 'FROM_END') { + const at = Blockly.JavaScript.getAdjusted(block, 'AT', 1, true); + code = `${text}.slice(${at}).charAt(0)`; + } else if (where === 'FIRST') { + code = `${text}.charAt(0)`; + } else if (where === 'LAST') { + code = `${text}.slice(-1)`; + } else if (where === 'RANDOM') { + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.provideFunction_('textRandomLetter', [ + // eslint-disable-next-line no-underscore-dangle + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(text) { + var x = Math.floor(Math.random() * text.length); + return text[x]; + }`, + ]); + code = `${functionName}(${text})`; + } + + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Text/text_getSubstring.js b/src/scratch/blocks/Text/text_getSubstring.js new file mode 100755 index 00000000..7786a5c9 --- /dev/null +++ b/src/scratch/blocks/Text/text_getSubstring.js @@ -0,0 +1,176 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.text_getSubstring = { + init() { + this.WHERE_OPTIONS_1 = [ + [translate('letter\u00A0#'), 'FROM_START'], + [translate('letter\u00A0#\u00A0from end'), 'FROM_END'], + [translate('first'), 'FIRST'], + ]; + this.WHERE_OPTIONS_2 = [ + [translate('letter\u00A0#'), 'FROM_START'], + [translate('letter\u00A0#\u00A0from end'), 'FROM_END'], + [translate('last'), 'LAST'], + ]; + + this.jsonInit({ + message0: translate('in text %1 get substring from %2 %3 to %4 %5'), + args0 : [ + { + type: 'input_value', + name: 'STRING', + }, + { + type : 'field_dropdown', + name : 'WHERE1', + options: this.WHERE_OPTIONS_1, + }, + { + type: 'input_dummy', + name: 'AT1', + }, + { + type : 'field_dropdown', + name : 'WHERE2', + options: this.WHERE_OPTIONS_2, + }, + { + type: 'input_dummy', + name: 'AT2', + }, + ], + output : 'String', + outputShape : Blockly.OUTPUT_SHAPE_SQUARE, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + + this.updateAt(1, true); + this.updateAt(2, true); + }, + mutationToDom() { + const container = document.createElement('mutation'); + const isAt1 = this.getInput('AT1').type === Blockly.INPUT_VALUE; + const isAt2 = this.getInput('AT2').type === Blockly.INPUT_VALUE; + + container.setAttribute('at1', isAt1); + container.setAttribute('at2', isAt2); + + return container; + }, + domToMutation(xmlElement) { + const isAt1 = xmlElement.getAttribute('at1') === 'true'; + const isAt2 = xmlElement.getAttribute('at2') === 'true'; + + this.updateAt(1, isAt1); + this.updateAt(2, isAt2); + }, + updateAt(n, isAt) { + this.removeInput(`AT${n}`, true); + if (isAt) { + this.appendValueInput(`AT${n}`).setCheck('Number'); + } else { + this.appendDummyInput(`AT${n}`); + } + + const menu = new Blockly.FieldDropdown(this[`WHERE_OPTIONS_${n}`], value => { + const newAt = ['FROM_START', 'FROM_END'].includes(value); + if (newAt !== isAt) { + this.updateAt(n, newAt); + this.setFieldValue(value, `WHERE${n}`); + return null; + } + return undefined; + }); + + this.getInput(`AT${n}`).appendField(menu, `WHERE${n}`); + if (n === 1) { + this.moveInputBefore('AT1', 'AT2'); + } + + this.initSvg(); + this.render(false); + }, +}; + +Blockly.JavaScript.text_getSubstring = block => { + const text = Blockly.JavaScript.valueToCode(block, 'STRING', Blockly.JavaScript.ORDER_FUNCTION_CALL) || '\'\''; + const where1 = block.getFieldValue('WHERE1'); + const where2 = block.getFieldValue('WHERE2'); + + let at1, + at2, + code; + + if (where1 === 'FIRST' && where2 === 'LAST') { + code = text; + } else if ( + text.match(/^'?\w+'?$/) || + (where1 !== 'FROM_END' && where1 !== 'LAST' && where2 !== 'FROM_END' && where2 !== 'LAST') + ) { + if (where1 === 'FROM_START') { + at1 = Blockly.JavaScript.getAdjusted(block, 'AT1'); + } else if (where1 === 'FROM_END') { + at1 = Blockly.JavaScript.getAdjusted(block, 'AT1', 1, false, Blockly.JavaScript.ORDER_SUBTRACTION); + at1 = `${text}.length - ${at1}`; + } else if (where1 === 'FIRST') { + at1 = '0'; + } + + if (where2 === 'FROM_START') { + at2 = Blockly.JavaScript.getAdjusted(block, 'AT2'); + } else if (where2 === 'FROM_END') { + at2 = Blockly.JavaScript.getAdjusted(block, 'AT2', 0, false, Blockly.JavaScript.ORDER_SUBTRACTION); + at2 = `${text}.length - ${at2}`; + } else if (where2 === 'LAST') { + at2 = `${text}.length`; + } + } else { + at1 = Blockly.JavaScript.getAdjusted(block, 'AT1'); + at2 = Blockly.JavaScript.getAdjusted(block, 'AT2'); + + // binary-bot: Blockly.JavaScript.text.getIndex_ (Blockly) + const getIndex = (stringName, where, optAt) => { + if (where === 'FIRST') { + return '0'; + } else if (where === 'FROM_END') { + return `${stringName}.length - 1 - ${optAt}`; + } else if (where === 'LAST') { + return `${stringName}.length - 1`; + } + return optAt; + }; + const wherePascalCase = { + FIRST : 'First', + LAST : 'Last', + FROM_START: 'FromStart', + FROM_END : 'FromEnd', + }; + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.provideFunction_( + `subsequence${wherePascalCase[where1]}${wherePascalCase[where2]}`, + [ + // eslint-disable-next-line no-underscore-dangle + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}( + sequence + ${where1 === 'FROM_END' || where1 === 'FROM_START' ? ', at1' : ''} + ${where2 === 'FROM_END' || where2 === 'FROM_START' ? ', at2' : ''} + ) { + var start = ${getIndex('sequence', where1, 'at1')}; + var end = ${getIndex('sequence', where2, 'at2')}; + return sequence.slice(start, end); + }`, + ] + ); + + code = `${functionName}( + ${text} + ${where1 === 'FROM_END' || where1 === 'FROM_START' ? `, ${at1}` : ''} + ${where2 === 'FROM_END' || where2 === 'FROM_START' ? `, ${at2}` : ''} + )`; + } + + code = `${text}.slice(${at1}, ${at2})`; + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Text/text_indexOf.js b/src/scratch/blocks/Text/text_indexOf.js new file mode 100755 index 00000000..89e7aebc --- /dev/null +++ b/src/scratch/blocks/Text/text_indexOf.js @@ -0,0 +1,43 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.text_indexOf = { + init() { + this.jsonInit({ + message0: translate('in text %1 find %2 occurence of text %3'), + args0 : [ + { + type: 'input_value', + name: 'VALUE', + // check: 'String', Rendering looks off when check is enabled. + }, + { + type : 'field_dropdown', + name : 'END', + options: [[translate('first'), 'FIRST'], [translate('last'), 'LAST']], + }, + { + type: 'input_value', + name: 'FIND', + // check: 'String', + }, + ], + output : 'String', + outputShape : Blockly.OUTPUT_SHAPE_SQUARE, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.text_indexOf = block => { + const functionName = block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; + const substring = Blockly.JavaScript.valueToCode(block, 'FIND', Blockly.JavaScript.ORDER_NONE) || '\'\''; + const text = Blockly.JavaScript.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_MEMBER) || '\'\''; + + const code = `${text}.${functionName}(${substring})`; + if (block.workspace.options.oneBasedIndex) { + return [`${code} + 1`, Blockly.JavaScript.ORDER_ADDITION]; + } + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Text/text_isEmpty.js b/src/scratch/blocks/Text/text_isEmpty.js new file mode 100755 index 00000000..aeffa106 --- /dev/null +++ b/src/scratch/blocks/Text/text_isEmpty.js @@ -0,0 +1,29 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.text_isEmpty = { + init() { + this.jsonInit({ + message0: translate('text %1 is empty'), + args0 : [ + { + type : 'input_value', + name : 'VALUE', + check: ['String'], + }, + ], + output : 'Boolean', + outputShape : Blockly.OUTPUT_SHAPE_HEXAGONAL, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.text_isEmpty = block => { + const text = Blockly.JavaScript.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_MEMBER) || '\'\''; + const isVariable = block.workspace.getAllVariables().findIndex(variable => variable.name === text) !== -1; + + const code = isVariable ? `!${text} || !${text}.length` : `!${text}.length`; + return [code, Blockly.JavaScript.ORDER_LOGICAL_NOT]; +}; diff --git a/src/scratch/blocks/Text/text_join.js b/src/scratch/blocks/Text/text_join.js new file mode 100755 index 00000000..a8801d3c --- /dev/null +++ b/src/scratch/blocks/Text/text_join.js @@ -0,0 +1,126 @@ +import { plusIconDark } from '../images'; +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.text_join = { + init() { + this.jsonInit({ + message0: translate('set %1 to create text with'), + message1: '%1', + args0 : [ + { + type : 'field_variable', + name : 'VARIABLE', + variable: translate('text'), + }, + ], + args1: [ + { + type: 'input_statement', + name: 'STACK', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + const fieldImage = new Blockly.FieldImage(plusIconDark, 25, 25, '', this.onIconClick.bind(this)); + + this.appendDummyInput('ADD_ICON').appendField(fieldImage); + this.moveInputBefore('ADD_ICON', 'STACK'); + }, + onIconClick() { + if (!this.workspace || this.isInFlyout) { + return; + } + + Blockly.Events.setGroup(true); + + const textBlock = this.workspace.newBlock('text_statement'); + textBlock.requiredParentId = this.id; + textBlock.setMovable(false); + textBlock.initSvg(); + textBlock.render(); + + const shadowBlock = this.workspace.newBlock('text'); + shadowBlock.setShadow(true); + shadowBlock.setFieldValue('', 'TEXT'); + shadowBlock.initSvg(); + shadowBlock.render(); + + const textInput = textBlock.getInput('TEXT'); + textInput.connection.connect(shadowBlock.outputConnection); + + const connection = this.getLastConnectionInStatement('STACK'); + connection.connect(textBlock.previousConnection); + + Blockly.Events.setGroup(false); + + // TODO: Open editor and focus so user can add string right away? + // const inputField = shadowBlock.getField('TEXT'); + // inputField.showEditor_(); + }, + enforceTextStatementType() { + let currentBlock = this.getInputTargetBlock('STACK'); + + while (currentBlock !== null) { + if (currentBlock.type !== 'text_statement') { + currentBlock.unplug(false); + } + currentBlock = currentBlock.getNextBlock(); + } + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (event.type === Blockly.Events.END_DRAG) { + // Only allow `text_statement` type blocks + const blocksInStatement = this.getBlocksInStatement('STACK'); + blocksInStatement.forEach(block => { + if (block.type !== 'text_statement') { + Blockly.Events.disable(); + block.unplug(); + Blockly.Events.enable(); + } + }); + } + }, +}; + +Blockly.JavaScript.text_join = block => { + // eslint-disable-next-line no-underscore-dangle + const varName = Blockly.JavaScript.variableDB_.getName( + block.getFieldValue('VARIABLE'), + Blockly.Variables.NAME_TYPE + ); + + const elements = []; + + let currentBlock = block.getInputTargetBlock('STACK'); + + while (currentBlock !== null) { + const value = Blockly.JavaScript[currentBlock.type](currentBlock); + + if (Array.isArray(value) && value.length === 2) { + elements.push(value[0]); + } else { + elements.push(value); + } + + currentBlock = currentBlock.getNextBlock(); + } + + let code; + + if (elements.length === 0) { + code = `${varName} = '';\n`; + } else { + code = `${varName} = ${elements.join(' + ')};\n`; + } + + return code; +}; diff --git a/src/scratch/blocks/Text/text_length.js b/src/scratch/blocks/Text/text_length.js new file mode 100755 index 00000000..65e794ca --- /dev/null +++ b/src/scratch/blocks/Text/text_length.js @@ -0,0 +1,27 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.text_length = { + init() { + this.jsonInit({ + message0: translate('length of %1'), + args0 : [ + { + type: 'input_value', + name: 'VALUE', + }, + ], + output : 'Number', + outputShape : Blockly.OUTPUT_SHAPE_ROUND, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.text_length = block => { + const text = Blockly.JavaScript.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_FUNCTION_CALL) || '\'\''; + + const code = `${text}.length`; + return [code, Blockly.JavaScript.ORDER_MEMBER]; +}; diff --git a/src/scratch/blocks/Text/text_print.js b/src/scratch/blocks/Text/text_print.js new file mode 100755 index 00000000..4178ee54 --- /dev/null +++ b/src/scratch/blocks/Text/text_print.js @@ -0,0 +1,26 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.text_print = { + init() { + this.jsonInit({ + message0: translate('print %1'), + args0 : [ + { + type: 'input_value', + name: 'TEXT', + }, + ], + colour : Blockly.Colours.Binary.colour, + colourSecondary : Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + }, +}; + +Blockly.JavaScript.text_print = block => { + const msg = Blockly.JavaScript.valueToCode(block, 'TEXT', Blockly.JavaScript.ORDER_NONE) || '\'\''; + const code = `window.alert(${msg});\n`; + return code; +}; diff --git a/src/scratch/blocks/Text/text_prompt_ext.js b/src/scratch/blocks/Text/text_prompt_ext.js new file mode 100755 index 00000000..0ec135aa --- /dev/null +++ b/src/scratch/blocks/Text/text_prompt_ext.js @@ -0,0 +1,62 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.text_prompt_ext = { + init() { + this.jsonInit({ + message0: translate('prompt for %1 with message %2'), + args0 : [ + { + type : 'field_dropdown', + name : 'TYPE', + options: [[translate('string'), 'TEXT'], [translate('number'), 'NUMBER']], + }, + { + type: 'input_value', + name: 'TEXT', + }, + ], + output : 'String', + outputShape : Blockly.OUTPUT_SHAPE_SQUARE, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + + // Change shape based on selected type + const typeField = this.getField('TYPE'); + typeField.setValidator(value => { + if (value === 'TEXT') { + this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE); + this.setOutput(true, 'String'); + } else if (value === 'NUMBER') { + this.setOutputShape(Blockly.OUTPUT_SHAPE_ROUND); + this.setOutput(true, 'Number'); + } + this.initSvg(); + this.render(false); + return undefined; + }); + }, +}; + +Blockly.JavaScript.text_prompt_ext = block => { + let msg, + code; + + if (block.getField('TEXT')) { + // Internal message + // eslint-disable-next-line no-underscore-dangle + msg = Blockly.JavaScript.quote_(block.getFieldValue('TEXT')); + } else { + // External message + msg = Blockly.JavaScript.valueToCode(block, 'TEXT', Blockly.JavaScript.ORDER_NONE) || '\'\''; + } + + if (block.getFieldValue('TYPE') === 'NUMBER') { + code = `parseFloat(window.prompt(${msg}))`; + } else { + code = `window.prompt(${msg})`; + } + + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/Text/text_statement.js b/src/scratch/blocks/Text/text_statement.js new file mode 100755 index 00000000..47b965a7 --- /dev/null +++ b/src/scratch/blocks/Text/text_statement.js @@ -0,0 +1,64 @@ +import { minusIconDark } from '../images'; + +Blockly.Blocks.text_statement = { + init() { + this.requiredParentId = ''; + + this.jsonInit({ + message0: '%1', + args0 : [ + { + type: 'input_value', + name: 'TEXT', + }, + ], + colour : Blockly.Colours.BinaryLessGray.colour, + colourSecondary : Blockly.Colours.BinaryLessGray.colourSecondary, + colourTertiary : Blockly.Colours.BinaryLessGray.colourTertiary, + previousStatement: null, + nextStatement : null, + }); + + const fieldImage = new Blockly.FieldImage(minusIconDark, 25, 25, '', () => this.onIconClick()); + + this.appendDummyInput('REMOVE_ICON').appendField(fieldImage); + }, + onIconClick() { + if (!this.workspace || this.isInFlyout) { + return; + } + + this.unplug(true); + this.dispose(); + }, + onchange(event) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + const surroundParent = this.getSurroundParent(); + if (event.type === Blockly.Events.END_DRAG) { + if (!this.requiredParentId && surroundParent.type === 'text_join') { + this.requiredParentId = surroundParent.id; + } else if (!surroundParent || surroundParent.id !== this.requiredParentId) { + Blockly.Events.disable(); + this.unplug(false); + + const parentBlock = this.workspace.getAllBlocks().find(block => block.id === this.requiredParentId); + + if (parentBlock) { + const parentConnection = parentBlock.getLastConnectionInStatement('STACK'); + parentConnection.connect(this.previousConnection); + } else { + this.dispose(); + } + Blockly.Events.enable(); + } + } + }, +}; + +Blockly.JavaScript.text_statement = block => { + const code = Blockly.JavaScript.valueToCode(block, 'TEXT') || ''; + return [code, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/scratch/blocks/Text/text_trim.js b/src/scratch/blocks/Text/text_trim.js new file mode 100755 index 00000000..1bf2a13e --- /dev/null +++ b/src/scratch/blocks/Text/text_trim.js @@ -0,0 +1,43 @@ +import { translate } from '../../../utils/lang/i18n'; + +Blockly.Blocks.text_trim = { + init() { + this.jsonInit({ + message0: translate('trim spaces from %1 of %2'), + args0 : [ + { + type : 'field_dropdown', + name : 'MODE', + options: [ + [translate('both sides'), 'BOTH'], + [translate('left side'), 'LEFT'], + [translate('right side'), 'RIGHT'], + ], + }, + { + type: 'input_value', + name: 'TEXT', + }, + ], + output : 'String', + outputShape : Blockly.OUTPUT_SHAPE_SQUARE, + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + }); + }, +}; + +Blockly.JavaScript.text_trim = block => { + const operators = { + LEFT : '.replace(/^[\\s\\xa0]+/, \'\')', + RIGHT: '.replace(/[\\s\\xa0]+$/, \'\')', + BOTH : '.trim()', + }; + + const operator = operators[block.getFieldValue('MODE')]; + const text = Blockly.JavaScript.valueToCode(block, 'TEXT', Blockly.JavaScript.ORDER_MEMBER) || '\'\''; + + const code = `${text}${operator}`; + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/scratch/blocks/images.js b/src/scratch/blocks/images.js new file mode 100644 index 00000000..b2e10996 --- /dev/null +++ b/src/scratch/blocks/images.js @@ -0,0 +1,32 @@ +export const defineContract = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAAGXcA1uAAAAAXNSR0IArs4c6QAAAK9JREFUSA3tVUkOgCAMROP/f4PfU4iOaWQKLYmBg71AOksXTAyhGkeKKuECjTSDUwcFxVdNawNgQ10omJNKROqiJbMJsKJZgKgEouuESRYVFVxO35JTnxGDvk9aWQ5GCXfSPbRbsLHysr0lBeMESaKE3hkmbKk230SY9Ulky5rG/SVJU8udPrMU5s7w9bEugUlN886MWiJN86/o2dywFQ17g10b+dlJefH9X0t9X+YETM6gfmoeI+MAAAAASUVORK5CYII='; + +export const purchase = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAAEgBckRAAAAAXNSR0IArs4c6QAAAvlJREFUaAXtmU1u00AUxxPEGoluKMcgJ8gCARsW9CZ0ReEU5BwFiYpWqtgYiQuQFSygJ2CRLV2E39+ZZ42dGc84XxIwT3p9X//34VfbsZPRaGNaQkpGPNu4yGESGfHUOtnYte0b6G8NdABJu7NoG4KXmi1GwUTACwtYomzp5r9ripP3/KB8zj7q4P4t0z9o9PfJowM0NhD6D9NN+sFm1xb05Rjybe171nKkDBLqfw6yt1Oqjjo3F4XArbli1dfmT7YpgOQGWPanLgjfFXyn6x9sU+QMfhdKxL/1OfSAGreh4vIR643H8pQ4hydRQCAAPv9owJ7AF1YH/bKr+z7FsPMbdBP8ZNNNOuwE+6v0bPILpJLAXsAvUrgSLxsoG4hsgAtoDDe3CoPhW7uFW8yXOffzcxK++ElOfxzwDXcxafAmhvscfjO8opdBgSfwjedqVPxaXbB5A0op5P+GH8ZwqXgwj6RtaO1kiDYJBiJOJop+HuScRZGyLfcUq2p5nJHVgAnrQzepXF/HnMIVnEckt84Os02qSkdfYN/Pq75KnpPQPFVYMZOBBq2B/EaxFVWApga0x2WT8vu64UIyq0EoMdfXeuC3JFZxhP7L7ByZe0Q5tQqmbKBsoGygbOCv2wAfHU/1SQp9Sw0P5nuNJCeFPUicYY7hW8fHqaYOr4dR5UQfSFN1dhJnAD1a/4RF2RsFq8d10Q0ce+LYyYy9RWiulw/R615gIEiOvuASpb/IDeRv7aLxq7r9FgOQbwvY7g2s72ho0vurgjuIfYmPfbMNijGhvsoSnQxKHABW7brDcvkhN23IBVS5otPc4hvgrHa1QW5/CpvR26loHkKuQuuvc/L7+BDO4sT0tiZ6ZL6dSgrr/VGkd54WrdzpYUM4FVJNF1u0CieM7g90CfjoM4Dn8BTu3v6u8LW2jZ3rA1rXlKz0Zy/Ehk7dlma7bkDdmav9cte1m3o0mLgmweugAW6gUHfuau/n/LeZaHLtGu1DXFufIssGygb+kw38ARimnBqQbBHiAAAAAElFTkSuQmCC'; + +export const sellContract = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAAEgBckRAAAAAXNSR0IArs4c6QAAA45JREFUaAXtWDFrFUEQzoU0QhpjFVRs/Cc2Sf6AxD4BQSF/QBFtbPwDYici2ImFIqaVlBEEK7FICBaxCFEQRM/v29vZzO7O3dv3uGCEXdg3M998M7O7d7e37+bmZmpt2y4xEPLITABHazpOC0wLwt5wtVKHDGBeFJEg7og+okTaVY5hUgslSQwGFJhb2tZ6mARI3XQ77x/YbzQR9mlMTpc4u3rDoWEFomXtGe5+0zSXg28oCL69IT8r/kwJYocLF0p1yi2ITwl2YiL6+MTq5iQZiUNvtO24GaAywLc+5FfUqp7tFdCXUeulo0bMMfoy+WY8QdUcMU0O/xfPuZ/6fOJtyZH56chABcC94oMXFWyqOlffs2YFXge4hi3gu05gEU3Mj24ZklvDtkUCfgHdfMiBu+cXMnos3QbGZHDsQVyyEs+CYaYh9yzxNaauQF2BWVaATzL6WG01GwMzE4TgfhSdGzKyAUi8z5HvzEKA5Ez08TOkA34jGInCeDT5H2EX6Di9O+k17+d2nW1kwDjz0JL6Pa85xfKRS5BP0T8oV6aSWwYqFoPQHiqoVyUxc5pgwgLnM3loFxNXZJIQATRMULHgf+5596AfKlemmrkIou2gZxfQJ6b/Lfov9CdZVgDAN9B5fLFnANy9TyH7ijyD72pPch5ZeIu7uynjwOGqQrDI1H+zJJ6Jtb6gK2mH1jVnSFcx+0O86qsrUFegrkBdgboC/9MKZK91vPBG/ac88mLEnxmt5Hxjs2kfTDnHTH3M0HlKdNTioYxtXfM7KB6X9gddiJA8OHHgPESx8UCVXbEQaCjg30bnMf+c4TYh1kCX702szTFwLK6ZQRoUIiSPllwNHjOLBw7uFfQf6GxH6LtO635u6lpDOuicCGtzDO6YCxndGWY8SWymswBE6F2XoG0PIN3EIVfQ36G/KkhhUhDrmunUYDFRByU6cvBsL+0Qyh30iV8QkzSRKcki0DKKiVZwgiHXPDrv4W8+74uEUmz6+Ml3RjGxpzTiP/ocL4UC+5HHvgo2rfTx5ROQAC+5K5hfsNOBgLeAzgH/RtftNYzzKd+yweOuIzuRzlE+AUmMaH6QkK3U/Owi3FQi7gH6+xQfssHnzsPGmu5zDfkOwc9QrPNZRGCyD0/9YWpiwYSAWrJlRlfcGhdDo//dPtca5GMGeFuLxR5cc8bSuQ3rXPwvv6mBqtcVqCtQV+Dfr8BfZ6VurZP8obgAAAAASUVORK5CYII='; + +export const finishSign = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAAEgBckRAAAAAXNSR0IArs4c6QAAAa5JREFUaAXtV1tywzAIdDq5Rg/Ui/SGzYl8j1brzqYYidHDYWw60g/iIWABKc6yGOsG+XdaSr/uFLe0dkZkNC25Wt6U7zq7C05zJMH9FofBd0IymZIKR4qYWAyh95LfbCgAxYLwd/e3h+yJm0YQWiurk2V4cXkzaALO5owKAJV7E7gsr9zzQBaBiirVXaV3KYeTbtDVyNOgUIFU9q+CeFj0bBs9sL/k5fhJnSXHOakbnzxmUKH3kl5m0JK1ZQ/fxQBHncqk3Uskg217mX2mHBC4I3APMAB6HglfgXQPXvqaZgXBRTu4PqTT+M+1+00uPtfW+94rRy/cEbgHKJYIY8pRGykLz/7jEkmIslwt8mSzSrtsbznMDBsF7lM0A1Q74V6iagbTYFZgVsC7Ani7sbzjjPrPPhu1o4skv6YPj3edG/jDAOQXjQxgAe+1p0/rXPhfgvAAih+9bJukVgtfNSq9/plb+A6EB9A8QmePCkdG0/AdCA+geYR068hbo0W9pr326Tz+p35qP+SbAVjPHB2dRcOP0ARw1ugwbvgOtFziR0J72T807MSkswKDFfgBlTxsE+QpCHoAAAAASUVORK5CYII='; + +export const caution = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABsklEQV +RoQ+1YS07DMBB9VlG2tJuWZVDYU04CN6BHKCfAnACOADcoJyHsiZRtu0m7rYiMItIFldqZ2ONaEc7Wo5d5n7ETK/T8UT3vH5FAaA +dZDlQ6TbcYPBtgqqBSn00bmFIBeYL6YaTLknoXSaBt/gNQQwpMdt2sE9Q3FAmSwPIxW0CpW9nmmGjGvE+eirtj1TQBnVWnV/+35S +ZOF7q4dCRwZZh6eSmb6K+jIjMc+KcE9pVbajshgjkQCbQTFR2Qyq4Uzv5W520XijMQZ6BVQCq7UjhxBrgfPHGIQw8x1ymqLthJTD +XGXY8EbLc/rsJUXTAH4i4UeheKDoR2gBpO7rr7EPf9Yqu9WswBdc5VTabObBLUU+erxaaZlU6nBmevAK5lmiNRPhW+Z2Nd5lQl+U +u5A6h0Otxi8AKoewrUbd28JajnI12uOThsAjuwlc5mBmiICEfKbBQwH+uicZr9dCbgKVLsyHT+IzskhVykukVGjIB7pOwiI07AMl +LWkfFCoAHlR8otMt4I0JGSiYx3AgciJRaZkxD4Gymgy8HEPgDaQqtzoOtLfNZHAj7V5WD33oEfb0MpQATTN7IAAAAASUVORK5CYI +I=`; + +export const plusIconDark = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAC+0lEQVR4Ae2bA4xeURCFp7bdoGvjv3OC2m1Q24xW0cZJ3caqzdhJ3Vi1bdu2po1T3v95Nvu+E/N+z5hDMf4RExMTE4N0HmkqsZS34RAu4iG+8kd+hEs4xNuw1FTySKSTRvIbYyLW4xbEHr7JG8z47CakBVOMlXgHcRZ+w6tRSNGCOmY874e4D+83E1CHoqCwBWbzI4gPuY85hS0oVGrwDDyD+JgnZibVoHDgXByCBJBDnBvG8kfiPSSgvOfBFCg1sRASbHhRYIdSSn3eDgk+vD2lPvlPfks+AgknfCS/pd8XzTRchYSYq4Vp5B/ogNuQkHPbdPTv4LkMiSCXclr5curiOCSinMivS17hZZAIs5S8YfpDIs5Ack9OKzyJWoAfozW5BVsgCrKF3GG6QHQk0ZVcUBsXIe5Dv+FJ4aKLlx6UQtQICEodb3++qUmAb1BtcgJPh2gSgGAaOQEX1QlcouQxPSHqBMT0dH791yDg/H5Q1Ig/qhT4UNSIksGMgqgUEDPK+fOnKgFeRsmAM1oFcCap9y+IWgFJ4mXf9NMsYPqRDZRpFkCZXWCJaoElZIP3aBbgPWQDp50v0D2OBU+TDb6pWYBv2gWeqxZ4bhf4rlrgm13gk2qBT2QDTzUL4CnZ4BuaBfhGNbiM6r6R7SYbWFrVHyUqNAtwOdnAQM0CiQFkI7uJZoH8xmSHT2kV4FOUDFiqVQBLq8dnlaJG+KBRgD8WNaLkwGaVApsoWbiHSoEe1eDzuuIfHDzd6S+me6oE7lNtcgZXaBLgchczoZp+s1Jtck6iK0RHTJdqOWrwE7Tmx5CI8wStyT0YGLWA6V+lB554mR8jZ8ciEzjmywRpTitcUj30Z8d0VD52aSeREvbgayIlHj3+83TmbaEsf1tK/eCaG4uUj9/bSQzDO9UFCDucy/sC2fb7ODe8EtB0/0tA4dew5uivYdlfeibyAW9FOJ6EOpFXEXk1v1VeRbR/0zYTHJdBNZLI/KOO+4kf4RIfDqKOGxMTExMT8wNOZdb6YlKT2wAAAABJRU5ErkJggg=='; + +export const plusIconLight = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAC+0lEQVR4Ae2bA4xeURCFp7bdoGvjv3OC2m1Q24xW0cZJ3caqzdhJ3Vi1bdu2po1T3v95Nvu+E/N+z5hDMf4RExMTE4N0HmkqsZS34RAu4iG+8kd+hEs4xNuw1FTySKSTRvIbYyLW4xbEHr7JG8z47CakBVOMlXgHcRZ+w6tRSNGCOmY874e4D+83E1CHoqCwBWbzI4gPuY85hS0oVGrwDDyD+JgnZibVoHDgXByCBJBDnBvG8kfiPSSgvOfBFCg1sRASbHhRYIdSSn3eDgk+vD2lPvlPfks+AgknfCS/pd8XzTRchYSYq4Vp5B/ogNuQkHPbdPTv4LkMiSCXclr5curiOCSinMivS17hZZAIs5S8YfpDIs5Ack9OKzyJWoAfozW5BVsgCrKF3GG6QHQk0ZVcUBsXIe5Dv+FJ4aKLlx6UQtQICEodb3++qUmAb1BtcgJPh2gSgGAaOQEX1QlcouQxPSHqBMT0dH791yDg/H5Q1Ig/qhT4UNSIksGMgqgUEDPK+fOnKgFeRsmAM1oFcCap9y+IWgFJ4mXf9NMsYPqRDZRpFkCZXWCJaoElZIP3aBbgPWQDp50v0D2OBU+TDb6pWYBv2gWeqxZ4bhf4rlrgm13gk2qBT2QDTzUL4CnZ4BuaBfhGNbiM6r6R7SYbWFrVHyUqNAtwOdnAQM0CiQFkI7uJZoH8xmSHT2kV4FOUDFiqVQBLq8dnlaJG+KBRgD8WNaLkwGaVApsoWbiHSoEe1eDzuuIfHDzd6S+me6oE7lNtcgZXaBLgchczoZp+s1Jtck6iK0RHTJdqOWrwE7Tmx5CI8wStyT0YGLWA6V+lB554mR8jZ8ciEzjmywRpTitcUj30Z8d0VD52aSeREvbgayIlHj3+83TmbaEsf1tK/eCaG4uUj9/bSQzDO9UFCDucy/sC2fb7ODe8EtB0/0tA4dew5uivYdlfeibyAW9FOJ6EOpFXEXk1v1VeRbR/0zYTHJdBNZLI/KOO+4kf4RIfDqKOGxMTExMT8wNOZdb6YlKT2wAAAABJRU5ErkJggg=='; + +export const minusIconDark = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAC6ElEQVR4Ae2bA6weURCFp7bd4NnaOyeo3Qa1zaiKXpzUbazajJ3UjVXbtm1rGqO6u/sv5uXf74t91nfvHEpISEhISAgO5PAwU4nlvANHcBmP8Z0/8xNcwRHegeWmkochhzRS0hDjsBF3IHb5Nm8yYwoakRZMBVbjA8Sb/I7XooziBbXMGD4I8S8fNGNRi+KgrBnm8hNIAD7EvLJmFCnVeCpeQAL0mZlG1SgauAhHICF4hIsofHgYPkJC8iMPoFCpjsWQcOUloV1KmXV5JyR8eWdmXQqekuZ8DBKNfKykOQVLWTauQyL0elk2BQfa4S4kYu+a9sFdPFchMXilsEUgty5OQmLyVEltShVeAYnR5ZQapg8kZvuRfwpb4FncAfgpWpJfsA2iwG3kD9MRokOnE/mgJi5DlHjZx6IHMyCKnOH5+PNtTQH4FtUkL/AUiDInkxdwWV2AK+Qe0w2iT9NN//M/mPdBeQP+rDLAp/IG5AYzHKJTM1z796dFXkFuwDmIUs+5Wn9B9OpisW96aw5gepMNzIQodqY9wDLVAZaRDd6nOQDvIxs4C3EjBYzLCGfJBt/WHIBv2wO8VB3gpT3AT9UBftgDfFEd4AvZwHPNAfCcbPAtzQH4Vho8RnW/yPaSDSyv6p8SszUH4FlkA/00B3D6ko2CRqoXNA3JDp+B6JTPkAXtt/Hy9PitUt4AnyD65M/lDcgd2KoywBZyC3dVGaBrGvxeV7zBwVO8bjE9UBXgIdUkb/Bs7d9AFlBL0zYr1STvOJ20BDAd03PUgAgt+SkkZp9Zhj20rw5Mnyo98MQrghg5OxFbgBOBTJAWtsAV7UN/Fkx7/WOXFpzMqAdfncxk9Pgvw987IOHLOzLrhtfcWKJ//N6CMxgftBcgLHARHwjl2B/gouhKQFOCLwFFX8Oap7+GZV/0jONDqRXheDxqxV5F5LX8XnkV0f5P24xVXAZ1j5P3Rx33Cz/BFT4aRh03ISEhISHhF9FSCRjypNzcAAAAAElFTkSuQmCC'; + +export const lockIconDark = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAACxklEQVR4Ae2aA4wdURSGT223Udf7Gizn/ieo2zg1Yqe23aC2jag2Yie7QW3bto2pMfehO53duTPJ+f54+A0vDgmCUPzkJamuWMM32P6Z61iruuYlUfBRqbyQz7EdJ+d4gUqloJJWEVPwlu3EwVuelFaRgge34etsFzHXrdZBu/Zr2HYXrAnMfcithQNsuw8O5NYi82SXxx62/y/YHalApon38OAT9mAjz/6a9diDT3EkVpNZVLeYJ3+V+3Bdxytel/vgVKx1VTcyh1WTn7Ot5QOmxHswrA58l20tzw2+CVjMtpaHVuOEL3wGburbYDGZgevinXYyd6y0f26Vwnc0gXeqHpkAU7TTf428Im2Xx6+192AqGaA0P9Su5EgqIjxIU39ApclvVCP96XfxZy2DC86tVSPyGx6jXf/xrrYeqOmPIb/BNucp5OWQC6wGmsBW8hs+6TiBG+QSre16gvxGe4ULyCUodL5B5Df47DiBDa633+hsOZHfaK/wCtcCK517CLaACIiACIiACOQkYwQK+SnbJZynKMSInGQqTrKr8k58Ztu/4DPvzK5KxUOkAo6x7X9wzMOokf60mglWkncQwSdjAp8QIa/wNLYNZhp5BUdNCuAYeYXfmBTgV94FbLMJpYAIiIAIiIAIiIAIiMB1zFItrJpf0xKz+HqoBPACnamUYzel0BkvQiKAtyqfYmBZeBsKAdWb4oBeYRC4lmDKtDSuBl4AYykBGBt4AQYlIF8FXiBSnRIQqV7SAiKQrygBlhV4gcRFAzwmDJ/RshSPsnwt1D8y1TskTQlGzD0Bb0PTmOOOemOOO+JFyJrTPFs157pc11xzWnpkIiACIiACIiACIiACz40KPJdSA8wwKjCDvKJSTZbbcAp5BytCWPCklZwdDV/JmVb0hx1+F/1hh9eiPw1O4eH+lF1yAQ8v2rMvCIIgCF8Av9j925HZ5hYAAAAASUVORK5CYII='; diff --git a/src/scratch/blocks/index.js b/src/scratch/blocks/index.js new file mode 100644 index 00000000..50c73aaf --- /dev/null +++ b/src/scratch/blocks/index.js @@ -0,0 +1,18 @@ +import './Advanced/Functions'; +import './Advanced/List'; +import './Advanced/Loops'; +import './Advanced/Variable'; + +import './Binary/After Purchase'; +import './Binary/Before Purchase'; +import './Binary/During Purchase'; +import './Binary/Indicators'; +import './Binary/Tick Analysis'; +import './Binary/Tools/Candle'; +import './Binary/Tools/Misc.'; +import './Binary/Tools/Time'; +import './Binary/Trade Definition'; + +import './Logic'; +import './Math'; +import './Text'; diff --git a/src/scratch/hooks/toolbox.js b/src/scratch/hooks/toolbox.js index ed9307bb..b0d8ce1b 100644 --- a/src/scratch/hooks/toolbox.js +++ b/src/scratch/hooks/toolbox.js @@ -1,4 +1,5 @@ /* eslint-disable func-names, no-underscore-dangle */ + /** * Fill the toolbox with categories and blocks. * @param {!Node} newTree DOM tree of blocks. @@ -23,24 +24,8 @@ Blockly.Toolbox.prototype.showCategory_ = function(category_id) { allContents = allContents.concat(category.getContents()); - // TEMP: For testing only, generate labels for each block for QA - let newAllContents = []; - if (Array.isArray(allContents) && allContents.length > 1) { - allContents.forEach(node => { - if (node.nodeName === 'block') { - const type = node.getAttribute('type'); - const labelString = ``; - - const labelXml = Blockly.Xml.textToDom(labelString); - newAllContents.push(...[labelXml.firstChild, node]); - } - }); - } else { - newAllContents = allContents; - } - this.flyout_.autoClose = true; - this.flyout_.show(newAllContents); + this.flyout_.show(allContents); }; /** @@ -73,7 +58,6 @@ Blockly.Toolbox.prototype.setSelectedItem = function(item) { * which does the actual refreshing. */ Blockly.Toolbox.prototype.refreshSelection = function() { - // this.showAll_(); }; /** diff --git a/src/scratch/index.js b/src/scratch/index.js index cba70e85..db75154d 100644 --- a/src/scratch/index.js +++ b/src/scratch/index.js @@ -1,3 +1,5 @@ +import './blocks'; +import './hooks'; import { isMainBlock, save, @@ -25,16 +27,18 @@ export const scratchWorkspaceInit = async (scratch_area_name, scratch_div_name) const toolbox_xml = await fetch('dist/toolbox.xml').then(response => response.text()); const main_xml = await fetch('dist/main.xml').then(response => response.text()); const workspace = Blockly.inject(scratch_div_name, { - media : 'dist/media/', - toolbox : toolbox_xml, + media : 'dist/media/', + toolbox: toolbox_xml, + grid : { + spacing: 40, + length : 11, + colour : '#ebebeb', + }, trashcan: true, zoom : { wheel: true, }, }); - - // TODO: Add GTM workspace listener for `Block Event` - // workspace.addChangeListener(() => {}); Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(main_xml), workspace); @@ -66,6 +70,7 @@ export const scratchWorkspaceInit = async (scratch_area_name, scratch_div_name) onWorkspaceResize(); } catch (error) { // TODO: Handle error. + throw error; } }; diff --git a/src/scratch/shared.js b/src/scratch/shared.js index dccb95ee..a7a8adc4 100644 --- a/src/scratch/shared.js +++ b/src/scratch/shared.js @@ -85,12 +85,15 @@ const getActiveMarket = markets => {} ); +/* eslint-disable no-unreachable */ fieldGeneratorMapping.MARKET_LIST = () => { + return [['', '']]; const markets = getActiveMarket(symbolApi.activeSymbols.getMarkets()); return Object.keys(markets).map(e => [markets[e].name, e]); }; fieldGeneratorMapping.SUBMARKET_LIST = block => () => { + return [['', '']]; const markets = getActiveMarket(symbolApi.activeSymbols.getMarkets()); const marketName = block.getFieldValue('MARKET_LIST'); if (!marketName || marketName === 'Invalid') { @@ -106,6 +109,7 @@ fieldGeneratorMapping.SUBMARKET_LIST = block => () => { }; fieldGeneratorMapping.SYMBOL_LIST = block => () => { + return [['', '']]; const markets = getActiveMarket(symbolApi.activeSymbols.getMarkets()); const submarketName = block.getFieldValue('SUBMARKET_LIST'); if (!submarketName || submarketName === 'Invalid') { @@ -121,6 +125,7 @@ fieldGeneratorMapping.SYMBOL_LIST = block => () => { .filter(symbol => !['frxGBPNOK', 'frxUSDNOK', 'frxUSDNEK', 'frxUSDSEK'].includes(symbol[1])) ); }; +/* eslint-enable */ fieldGeneratorMapping.TRADETYPECAT_LIST = block => () => { const symbol = block.getFieldValue('SYMBOL_LIST'); diff --git a/src/scratch/xml/main.xml b/src/scratch/xml/main.xml index fb7fb2f8..be943dde 100644 --- a/src/scratch/xml/main.xml +++ b/src/scratch/xml/main.xml @@ -1,37 +1,73 @@ - - - - - t - USD - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + bb + + + + + + + + + + + + + + + + + 60 + + + + + FALSE + + + + + + + FALSE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/scratch/xml/toolbox.xml b/src/scratch/xml/toolbox.xml index a8c4658f..594b9918 100644 --- a/src/scratch/xml/toolbox.xml +++ b/src/scratch/xml/toolbox.xml @@ -1,443 +1,666 @@ - - - - - - - - - - - - - 1 - - - - - 1 - - - - - 1 - - - - - - - 9 - - - - - - - 45 - - - - - - - - 0 - - - - - - - 1 - - - - - - - 3.1 - - - - - - - - 64 - - - - - 10 - - - - - - - 50 - - - - - 1 - - - - - 100 - - - - - - - 1 - - - - - 100 - - - - - - - > - - - - - - - - - - - abc - - - - - - - - - - - - - - text - - - - - abc - - - - - - - text - - - - - - - text - - - - - - - abc - - - - - - - abc - - - - - - - abc - - - - - - - abc - - - - - - - - - - - - - - 5 - - - - - - - - - list - - - - - - - list - - - - - - - list - - - - - - - list - - - - - - - , - - - - - - > - - - - - - - - - - - - - - - - - - 5 - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - open - - - 1 - - - - - - - - - 1 - - - - - - - - - - - - - - 10 - - - - - - - 10 - - - - - 1 - - - 10 - - - - - 5 - - - - - 5 - - - - - - - 10 - - - - - - - - 10 - - - - - - - 10 - - - - - 1 - - - 10 - - - - - 5 - - - - - 5 - - - - - - - 10 - - - - - 0 - - - 12 - - - - - 26 - - - - - 9 - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - success - - - abc - - - - - - - - + + + + + + + + + + + + + + + + + ADD + + + 1 + + + + + 1 + + + + + ROOT + + + 9 + + + + + SIN + + + 45 + + + + + + + EVEN + + + 0 + + + + + item + + + 1 + + + + + + ROUND + + + 3.1 + + + + + + + 64 + + + + + 10 + + + + + + + 50 + + + + + 1 + + + + + 100 + + + + + + + 1 + + + + + 100 + + + + + + + + abc + + + text + + + + + abc + + + + + + + text + + + abc + + + + + + + abc + + + + + + + + + + + + FIRST + + + text + + + + + abc + + + + + + FROM_START + + + item + + + + + 1 + + + + + + FROM_START + FROM_START + + + text + + + + + 0 + + + + + 2 + + + + + UPPERCASE + + + abc + + + + + BOTH + + + abc + + + + + + + abc + + + + + TEXT + + + abc + + + + + + + list + + + + + + + + + + list + + + 5 + + + + + + + + + + + + SPLIT + + + , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 60 + + + + + FALSE + + + + + + + FALSE + + + + + + + + + + + + + + + + + + + USD + + + 1 + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + open + default + + + 1 + + + + + + + + default + + + 1 + + + + + + + + + + + 0 + + + + + open + default + + + + + + + 10 + + + + + + + 5 + + + + + + + 5 + + + + + + + + + + + + + + + + + open + default + + + + + + + 10 + + + + + + + + + + + + + open + default + + + + + + + 10 + + + + + + + + + + + + + open + default + + + + + + + 10 + + + + + + + + + + + 0 + + + + + open + default + + + + + + + 10 + + + + + + + 5 + + + + + + + 5 + + + + + + + + + + + + + + + + + open + default + + + + + + + 10 + + + + + + + + + 1 + + + + + open + default + + + + + + + 12 + + + + + + + 26 + + + + + + + 9 + + + + + + + + + + + + + + + + + open + default + + + + + + + 10 + + + + + + + + + + + + + open + default + + + + + + + 10 + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + success + silent + + + abc + + + + + + diff --git a/webpack.config.js b/webpack.config.js index 0369b0da..7988b06e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,14 +1,13 @@ -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); -const path = require('path'); -const StyleLintPlugin = require('stylelint-webpack-plugin'); -const SpriteLoaderPlugin = require('svg-sprite-loader/plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const path = require('path'); +const StyleLintPlugin = require('stylelint-webpack-plugin'); +const SpriteLoaderPlugin = require('svg-sprite-loader/plugin'); +const MergeIntoSingleFilePlugin = require('webpack-merge-and-include-globally'); module.exports = { entry : [ - '@babel/polyfill', - path.join(__dirname, './src/scratch/hooks'), - path.join(__dirname, './src/app.js'), + path.join(__dirname, 'src', 'app.js') ], output: { path : path.resolve(__dirname, 'dist'), @@ -80,12 +79,16 @@ module.exports = { new CopyWebpackPlugin([ { from: './src/scratch/xml' }, { from: './node_modules/scratch-blocks/media', to: 'media' }, - { from: './node_modules/scratch-blocks/blockly_compressed_vertical.js', to: 'scratch-compressed.js' }, - { from: './node_modules/scratch-blocks/msg/messages.js', to: 'scratch-messages.js' }, ]), + new MergeIntoSingleFilePlugin({ + files: { + 'scratch.js': [ + './node_modules/scratch-blocks/blockly_compressed_vertical.js', + './node_modules/scratch-blocks/msg/messages.js', + './node_modules/blockly/generators/javascript.js', + ] + } + }), new SpriteLoaderPlugin(), ], - externals: { - Blockly: 'Blockly', - } -}; +}; \ No newline at end of file