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