@@ -169,7 +198,7 @@ export default function PublicCanvas({ activity, isSandbox }) {
>
- Program your Arduino...
+ {selectedFeature}
@@ -179,6 +208,9 @@ export default function PublicCanvas({ activity, isSandbox }) {
+
+ {featureList(notSelectedFeature, notSelectedFeature)}
+
@@ -323,4 +355,4 @@ export default function PublicCanvas({ activity, isSandbox }) {
)}
);
-}
+}
\ No newline at end of file
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/CustomBlock.jsx b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/CustomBlock.jsx
new file mode 100644
index 000000000..8fde0a098
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/CustomBlock.jsx
@@ -0,0 +1,459 @@
+import React, { useEffect, useRef, useState, useReducer } from 'react';
+import { Link } from 'react-router-dom';
+
+import '../../../ActivityLevels.less';
+import { compileArduinoCode } from '../../../Utils/helpers';
+import { message, Spin, Row, Col, Alert, Menu, Dropdown } from 'antd';
+import CodeModal from '../../modals/CodeModal';
+import ConsoleModal from '../../modals/ConsoleModal';
+import PlotterModal from '../../modals/PlotterModal';
+import {
+ connectToPort,
+ handleCloseConnection,
+ handleOpenConnection,
+} from '../../../Utils/consoleHelpers';
+import ArduinoLogo from '../../Icons/ArduinoLogo';
+import PlotterLogo from '../../Icons/PlotterLogo';
+import { getActivityToolbox } from '../../../../../Utils/requests';
+import PublicCanvas from '../PublicCanvas';
+import './blocks';
+
+
+let plotId = 1;
+
+export default function CustomBlock({ activity, isSandbox, workspace}) {
+ const [hoverUndo, setHoverUndo] = useState(false);
+ const [hoverRedo, setHoverRedo] = useState(false);
+ const [hoverCompile, setHoverCompile] = useState(false);
+ const [hoverConsole, setHoverConsole] = useState(false);
+ const [showConsole, setShowConsole] = useState(false);
+ const [showPlotter, setShowPlotter] = useState(false);
+ const [plotData, setPlotData] = useState([]);
+ const [connectionOpen, setConnectionOpen] = useState(false);
+ const [selectedCompile, setSelectedCompile] = useState(false);
+ const [compileError, setCompileError] = useState('');
+
+ // useStates for Program your Arduino... / Custom Blocks
+ const [selectedFeature, setSelectedFeature] = useState('Custom Blocks');
+ const [notSelectedFeature, setNotSelectedFeature] = useState('Program your Arduino...')
+ const [blockCode, setBlockCode] = useState('');
+ const [generatorCode, setGeneratorCode] = useState('');
+ const [arduinoCode, setArduinoCode] = useState('');
+
+ const [forceUpdate] = useReducer((x) => x + 1, 0);
+
+ const workspaceRef = useRef(null);
+ const activityRef = useRef(null);
+
+
+ // const xmlToBlockDefinition = (xmlText) => {
+
+ // };
+
+
+
+ const setWorkspace = () => {
+ workspaceRef.current = window.Blockly.inject('newblockly-canvas', {
+ toolbox: document.getElementById('toolbox'),
+ });
+
+
+ workspaceRef.current.addChangeListener(() => {
+ const xml = Blockly.Xml.workspaceToDom(workspaceRef.current);
+ const xmlText = Blockly.Xml.domToText(xml);
+ setBlockCode(xmlText);
+
+ const arduino = Blockly.Arduino.workspaceToDom(workspaceRef.current);
+ const arduinoText = Blockly.Arduino.domToText(arduino);
+ setBlockCode(arduinoTex);
+
+ const generatorCode = Blockly.JavaScript.workspaceToCode(workspaceRef.current);
+ setGeneratorCode(generatorCode);
+
+ const arduinoCode = Blockly.Arduino.workspaceToCode(workspaceRef.current);
+ setArduinoCode(arduinoCode);
+ });
+ };
+
+
+
+
+ // useEffect(() => {
+ // setInitialWorkspace();
+ // }, [workspace]);
+
+
+ useEffect(() => {
+ const setUp = async () => {
+ activityRef.current = activity;
+ if (!workspaceRef.current && activity && Object.keys(activity).length !== 0) {
+ setWorkspace();
+ }
+ };
+ setUp();
+ }, [activity]);
+
+ const handleUndo = () => {
+ if (workspaceRef.current.undoStack_.length > 0)
+ workspaceRef.current.undo(false);
+ };
+
+ const handleRedo = () => {
+ if (workspaceRef.current.redoStack_.length > 0)
+ workspaceRef.current.undo(true);
+ };
+
+ const handleConsole = async () => {
+ if (showPlotter) {
+ message.warning('Close serial plotter before openning serial monitor');
+ return;
+ }
+ // if serial monitor is not shown
+ if (!showConsole) {
+ // connect to port
+ await handleOpenConnection(9600, 'newLine');
+ // if fail to connect to port, return
+ if (typeof window['port'] === 'undefined') {
+ message.error('Fail to select serial device');
+ return;
+ }
+ setConnectionOpen(true);
+ setShowConsole(true);
+ }
+ // if serial monitor is shown, close the connection
+ else {
+ if (connectionOpen) {
+ await handleCloseConnection();
+ setConnectionOpen(false);
+ }
+ setShowConsole(false);
+ }
+ };
+
+ const handlePlotter = async () => {
+ if (showConsole) {
+ message.warning('Close serial monitor before openning serial plotter');
+ return;
+ }
+
+ if (!showPlotter) {
+ await handleOpenConnection(
+ 9600,
+ 'plot',
+ plotData,
+ setPlotData,
+ plotId,
+ forceUpdate
+ );
+ if (typeof window['port'] === 'undefined') {
+ message.error('Fail to select serial device');
+ return;
+ }
+ setConnectionOpen(true);
+ setShowPlotter(true);
+ } else {
+ plotId = 1;
+ if (connectionOpen) {
+ await handleCloseConnection();
+ setConnectionOpen(false);
+ }
+ setShowPlotter(false);
+ }
+ };
+
+ const handleCompile = async () => {
+ if (showConsole || showPlotter) {
+ message.warning(
+ 'Close Serial Monitor and Serial Plotter before uploading your code'
+ );
+ } else {
+ if (typeof window['port'] === 'undefined') {
+ await connectToPort();
+ }
+ if (typeof window['port'] === 'undefined') {
+ message.error('Fail to select serial device');
+ return;
+ }
+ setCompileError('');
+ await compileArduinoCode(
+ workspaceRef.current,
+ setSelectedCompile,
+ setCompileError,
+ activity,
+ false
+ );
+ }
+ };
+
+ const menu = (
+
+ );
+
+ //Program you Arduino... / Custom Blocks | switch
+ const featureList = (buttonText, newFeature) => (
+
+ );
+
+ const saveBlock = (buttonText) => (
+
+
+ );
+
+ if(selectedFeature === 'Program your Arduino...'){
+ return
;
+ }
+
+ return (
+
+
+
+
+
+
+
+ {/* Program your Arduino... / Custom Blocks */}
+ {selectedFeature}
+
+
+
+
+
+
+
+
+
+ {/* Custom Blocks / Program your Arduino... */}
+
+ {featureList(notSelectedFeature, notSelectedFeature)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {hoverCompile && (
+
+ Upload to Arduino
+
+ )}
+
+
handleConsole()}
+ className='fas fa-terminal hvr-info'
+ style={{ marginLeft: '6px' }}
+ onMouseEnter={() => setHoverConsole(true)}
+ onMouseLeave={() => setHoverConsole(false)}
+ />
+ {hoverConsole && (
+
+ Show Serial Monitor
+
+ )}
+
+
+
+
+
+
+
+
+ {/* Code to fix the workspace to half and provide space for the block def and gen code, will need to add a block preview */}
+
+ {saveBlock('Save Block')}
+ Block Preview
+
+ {/* Block Preview */}
+
+ Block Definition
+
+ {/* {Block Definition} */}
+ {blockCode}
+
+ Generator Stub
+
+ {/* {Generator Stub} */}
+ {arduinoCode}
+
+
+
+
+
+
+
+ {/* This xml is for the blocks' menu we will provide. Here are examples on how to include categories and subcategories */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 20
+ 65
+ 120
+ 160
+ 210
+ 230
+ 260
+ 290
+ 330
+
+
+
+
+ {compileError && (
+
setCompileError('')}
+ >
+ )}
+
+ );
+}
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks.js
new file mode 100644
index 000000000..c6c142395
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks.js
@@ -0,0 +1,778 @@
+/**
+ * Blockly Demos: Block Factory Blocks
+ *
+ * Copyright 2012 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Blocks for Blockly's Block Factory application.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+Blockly.Blocks['factory_base'] = {
+ // Base of new block.
+ init: function() {
+ this.setColour(120);
+ this.appendDummyInput()
+ .appendField('name')
+ .appendField(new Blockly.FieldTextInput('math_foo'), 'NAME');
+ this.appendStatementInput('INPUTS')
+ .setCheck('Input')
+ .appendField('inputs');
+ var dropdown = new Blockly.FieldDropdown([
+ ['automatic inputs', 'AUTO'],
+ ['external inputs', 'EXT'],
+ ['inline inputs', 'INT']]);
+ this.appendDummyInput()
+ .appendField(dropdown, 'INLINE');
+ dropdown = new Blockly.FieldDropdown([
+ ['no connections', 'NONE'],
+ ['← left output', 'LEFT'],
+ ['↕ top+bottom connections', 'BOTH'],
+ ['↑ top connection', 'TOP'],
+ ['↓ bottom connection', 'BOTTOM']],
+ function(option) {
+ this.sourceBlock_.updateShape_(option);
+ });
+ this.appendDummyInput()
+ .appendField(dropdown, 'CONNECTIONS');
+ this.appendValueInput('COLOUR')
+ .setCheck('Colour')
+ .appendField('colour');
+ this.setTooltip('Build a custom block by plugging\n' +
+ 'fields, inputs and other blocks here.');
+ this.setHelpUrl(
+ 'https://developers.google.com/blockly/custom-blocks/block-factory');
+ },
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ container.setAttribute('connections', this.getFieldValue('CONNECTIONS'));
+ return container;
+ },
+ domToMutation: function(xmlElement) {
+ var connections = xmlElement.getAttribute('connections');
+ this.updateShape_(connections);
+ },
+ updateShape_: function(option) {
+ var outputExists = this.getInput('OUTPUTTYPE');
+ var topExists = this.getInput('TOPTYPE');
+ var bottomExists = this.getInput('BOTTOMTYPE');
+ if (option == 'LEFT') {
+ if (!outputExists) {
+ this.addTypeInput_('OUTPUTTYPE', 'output type');
+ }
+ } else if (outputExists) {
+ this.removeInput('OUTPUTTYPE');
+ }
+ if (option == 'TOP' || option == 'BOTH') {
+ if (!topExists) {
+ this.addTypeInput_('TOPTYPE', 'top type');
+ }
+ } else if (topExists) {
+ this.removeInput('TOPTYPE');
+ }
+ if (option == 'BOTTOM' || option == 'BOTH') {
+ if (!bottomExists) {
+ this.addTypeInput_('BOTTOMTYPE', 'bottom type');
+ }
+ } else if (bottomExists) {
+ this.removeInput('BOTTOMTYPE');
+ }
+ },
+ addTypeInput_: function(name, label) {
+ this.appendValueInput(name)
+ .setCheck('Type')
+ .appendField(label);
+ this.moveInputBefore(name, 'COLOUR');
+ var type = this.workspace.newBlock('type_null');
+ type.setShadow(true);
+ type.outputConnection.connect(this.getInput(name).connection);
+ type.initSvg();
+ type.render();
+ }
+};
+
+var FIELD_MESSAGE = 'fields %1 %2';
+var FIELD_ARGS = [
+ {
+ "type": "field_dropdown",
+ "name": "ALIGN",
+ "options": [['left', 'LEFT'], ['right', 'RIGHT'], ['centre', 'CENTRE']],
+ },
+ {
+ "type": "input_statement",
+ "name": "FIELDS",
+ "check": "Field"
+ }
+];
+
+var TYPE_MESSAGE = 'type %1';
+var TYPE_ARGS = [
+ {
+ "type": "input_value",
+ "name": "TYPE",
+ "check": "Type",
+ "align": "RIGHT"
+ }
+];
+
+Blockly.Blocks['input_value'] = {
+ // Value input.
+ init: function() {
+ this.jsonInit({
+ "message0": "value input %1 %2",
+ "args0": [
+ {
+ "type": "field_input",
+ "name": "INPUTNAME",
+ "text": "NAME"
+ },
+ {
+ "type": "input_dummy"
+ }
+ ],
+ "message1": FIELD_MESSAGE,
+ "args1": FIELD_ARGS,
+ "message2": TYPE_MESSAGE,
+ "args2": TYPE_ARGS,
+ "previousStatement": "Input",
+ "nextStatement": "Input",
+ "colour": 210,
+ "tooltip": "A value socket for horizontal connections.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=71"
+ });
+ },
+ onchange: function() {
+ inputNameCheck(this);
+ }
+};
+
+Blockly.Blocks['input_statement'] = {
+ // Statement input.
+ init: function() {
+ this.jsonInit({
+ "message0": "statement input %1 %2",
+ "args0": [
+ {
+ "type": "field_input",
+ "name": "INPUTNAME",
+ "text": "NAME"
+ },
+ {
+ "type": "input_dummy"
+ },
+ ],
+ "message1": FIELD_MESSAGE,
+ "args1": FIELD_ARGS,
+ "message2": TYPE_MESSAGE,
+ "args2": TYPE_ARGS,
+ "previousStatement": "Input",
+ "nextStatement": "Input",
+ "colour": 210,
+ "tooltip": "A statement socket for enclosed vertical stacks.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=246"
+ });
+ },
+ onchange: function() {
+ inputNameCheck(this);
+ }
+};
+
+Blockly.Blocks['input_dummy'] = {
+ // Dummy input.
+ init: function() {
+ this.jsonInit({
+ "message0": "dummy input",
+ "message1": FIELD_MESSAGE,
+ "args1": FIELD_ARGS,
+ "previousStatement": "Input",
+ "nextStatement": "Input",
+ "colour": 210,
+ "tooltip": "For adding fields on a separate row with no " +
+ "connections. Alignment options (left, right, centre) " +
+ "apply only to multi-line fields.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=293"
+ });
+ }
+};
+
+Blockly.Blocks['field_static'] = {
+ // Text value.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('text')
+ .appendField(new Blockly.FieldTextInput(''), 'TEXT');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('Static text that serves as a label.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=88');
+ }
+};
+
+Blockly.Blocks['field_input'] = {
+ // Text input.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('text input')
+ .appendField(new Blockly.FieldTextInput('default'), 'TEXT')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('An input field for the user to enter text.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=319');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_angle'] = {
+ // Angle input.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('angle input')
+ .appendField(new Blockly.FieldAngle('90'), 'ANGLE')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('An input field for the user to enter an angle.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=372');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_dropdown'] = {
+ // Dropdown menu.
+ init: function() {
+ this.appendDummyInput()
+ .appendField('dropdown')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.optionCount_ = 3;
+ this.updateShape_();
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setMutator(new Blockly.Mutator(['field_dropdown_option']));
+ this.setColour(160);
+ this.setTooltip('Dropdown menu with a list of options.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386');
+ },
+ mutationToDom: function(workspace) {
+ // Create XML to represent menu options.
+ var container = document.createElement('mutation');
+ container.setAttribute('options', this.optionCount_);
+ return container;
+ },
+ domToMutation: function(container) {
+ // Parse XML to restore the menu options.
+ this.optionCount_ = parseInt(container.getAttribute('options'), 10);
+ this.updateShape_();
+ },
+ decompose: function(workspace) {
+ // Populate the mutator's dialog with this block's components.
+ var containerBlock = workspace.newBlock('field_dropdown_container');
+ containerBlock.initSvg();
+ var connection = containerBlock.getInput('STACK').connection;
+ for (var i = 0; i < this.optionCount_; i++) {
+ var optionBlock = workspace.newBlock('field_dropdown_option');
+ optionBlock.initSvg();
+ connection.connect(optionBlock.previousConnection);
+ connection = optionBlock.nextConnection;
+ }
+ return containerBlock;
+ },
+ compose: function(containerBlock) {
+ // Reconfigure this block based on the mutator dialog's components.
+ var optionBlock = containerBlock.getInputTargetBlock('STACK');
+ // Count number of inputs.
+ var data = [];
+ while (optionBlock) {
+ data.push([optionBlock.userData_, optionBlock.cpuData_]);
+ optionBlock = optionBlock.nextConnection &&
+ optionBlock.nextConnection.targetBlock();
+ }
+ this.optionCount_ = data.length;
+ this.updateShape_();
+ // Restore any data.
+ for (var i = 0; i < this.optionCount_; i++) {
+ this.setFieldValue(data[i][0] || 'option', 'USER' + i);
+ this.setFieldValue(data[i][1] || 'OPTIONNAME', 'CPU' + i);
+ }
+ },
+ saveConnections: function(containerBlock) {
+ // Store names and values for each option.
+ var optionBlock = containerBlock.getInputTargetBlock('STACK');
+ var i = 0;
+ while (optionBlock) {
+ optionBlock.userData_ = this.getFieldValue('USER' + i);
+ optionBlock.cpuData_ = this.getFieldValue('CPU' + i);
+ i++;
+ optionBlock = optionBlock.nextConnection &&
+ optionBlock.nextConnection.targetBlock();
+ }
+ },
+ updateShape_: function() {
+ // Modify this block to have the correct number of options.
+ // Add new options.
+ for (var i = 0; i < this.optionCount_; i++) {
+ if (!this.getInput('OPTION' + i)) {
+ this.appendDummyInput('OPTION' + i)
+ .appendField(new Blockly.FieldTextInput('option'), 'USER' + i)
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('OPTIONNAME'), 'CPU' + i);
+ }
+ }
+ // Remove deleted options.
+ while (this.getInput('OPTION' + i)) {
+ this.removeInput('OPTION' + i);
+ i++;
+ }
+ },
+ onchange: function() {
+ if (this.workspace && this.optionCount_ < 1) {
+ this.setWarningText('Drop down menu must\nhave at least one option.');
+ } else {
+ fieldNameCheck(this);
+ }
+ }
+};
+
+Blockly.Blocks['field_dropdown_container'] = {
+ // Container.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('add options');
+ this.appendStatementInput('STACK');
+ this.setTooltip('Add, remove, or reorder options\n' +
+ 'to reconfigure this dropdown menu.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386');
+ this.contextMenu = false;
+ }
+};
+
+Blockly.Blocks['field_dropdown_option'] = {
+ // Add option.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('option');
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setTooltip('Add a new option to the dropdown menu.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386');
+ this.contextMenu = false;
+ }
+};
+
+Blockly.Blocks['field_checkbox'] = {
+ // Checkbox.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('checkbox')
+ .appendField(new Blockly.FieldCheckbox('TRUE'), 'CHECKED')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('Checkbox field.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=485');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_colour'] = {
+ // Colour input.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('colour')
+ .appendField(new Blockly.FieldColour('#ff0000'), 'COLOUR')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('Colour input field.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=495');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_date'] = {
+ // Date input.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('date')
+ .appendField(new Blockly.FieldDate(), 'DATE')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('Date input field.');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_variable'] = {
+ // Dropdown for variables.
+ init: function() {
+ this.setColour(160);
+ this.appendDummyInput()
+ .appendField('variable')
+ .appendField(new Blockly.FieldTextInput('item'), 'TEXT')
+ .appendField(',')
+ .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('Dropdown menu for variable names.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=510');
+ },
+ onchange: function() {
+ fieldNameCheck(this);
+ }
+};
+
+Blockly.Blocks['field_image'] = {
+ // Image.
+ init: function() {
+ this.setColour(160);
+ var src = 'https://www.gstatic.com/codesite/ph/images/star_on.gif';
+ this.appendDummyInput()
+ .appendField('image')
+ .appendField(new Blockly.FieldTextInput(src), 'SRC');
+ this.appendDummyInput()
+ .appendField('width')
+ .appendField(new Blockly.FieldTextInput('15',
+ Blockly.FieldTextInput.numberValidator), 'WIDTH')
+ .appendField('height')
+ .appendField(new Blockly.FieldTextInput('15',
+ Blockly.FieldTextInput.numberValidator), 'HEIGHT')
+ .appendField('alt text')
+ .appendField(new Blockly.FieldTextInput('*'), 'ALT');
+ this.setPreviousStatement(true, 'Field');
+ this.setNextStatement(true, 'Field');
+ this.setTooltip('Static image (JPEG, PNG, GIF, SVG, BMP).\n' +
+ 'Retains aspect ratio regardless of height and width.\n' +
+ 'Alt text is for when collapsed.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=567');
+ }
+};
+
+Blockly.Blocks['type_group'] = {
+ // Group of types.
+ init: function() {
+ this.typeCount_ = 2;
+ this.updateShape_();
+ this.setOutput(true, 'Type');
+ this.setMutator(new Blockly.Mutator(['type_group_item']));
+ this.setColour(230);
+ this.setTooltip('Allows more than one type to be accepted.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677');
+ },
+ mutationToDom: function(workspace) {
+ // Create XML to represent a group of types.
+ var container = document.createElement('mutation');
+ container.setAttribute('types', this.typeCount_);
+ return container;
+ },
+ domToMutation: function(container) {
+ // Parse XML to restore the group of types.
+ this.typeCount_ = parseInt(container.getAttribute('types'), 10);
+ this.updateShape_();
+ for (var x = 0; x < this.typeCount_; x++) {
+ this.removeInput('TYPE' + x);
+ }
+ for (var x = 0; x < this.typeCount_; x++) {
+ var input = this.appendValueInput('TYPE' + x)
+ .setCheck('Type');
+ if (x == 0) {
+ input.appendField('any of');
+ }
+ }
+ },
+ decompose: function(workspace) {
+ // Populate the mutator's dialog with this block's components.
+ var containerBlock = workspace.newBlock('type_group_container');
+ containerBlock.initSvg();
+ var connection = containerBlock.getInput('STACK').connection;
+ for (var i = 0; i < this.typeCount_; i++) {
+ var typeBlock = workspace.newBlock('type_group_item');
+ typeBlock.initSvg();
+ connection.connect(typeBlock.previousConnection);
+ connection = typeBlock.nextConnection;
+ }
+ return containerBlock;
+ },
+ compose: function(containerBlock) {
+ // Reconfigure this block based on the mutator dialog's components.
+ var typeBlock = containerBlock.getInputTargetBlock('STACK');
+ // Count number of inputs.
+ var connections = [];
+ while (typeBlock) {
+ connections.push(typeBlock.valueConnection_);
+ typeBlock = typeBlock.nextConnection &&
+ typeBlock.nextConnection.targetBlock();
+ }
+ // Disconnect any children that don't belong.
+ for (var i = 0; i < this.typeCount_; i++) {
+ var connection = this.getInput('TYPE' + i).connection.targetConnection;
+ if (connection && connections.indexOf(connection) == -1) {
+ connection.disconnect();
+ }
+ }
+ this.typeCount_ = connections.length;
+ this.updateShape_();
+ // Reconnect any child blocks.
+ for (var i = 0; i < this.typeCount_; i++) {
+ Blockly.Mutator.reconnect(connections[i], this, 'TYPE' + i);
+ }
+ },
+ saveConnections: function(containerBlock) {
+ // Store a pointer to any connected child blocks.
+ var typeBlock = containerBlock.getInputTargetBlock('STACK');
+ var i = 0;
+ while (typeBlock) {
+ var input = this.getInput('TYPE' + i);
+ typeBlock.valueConnection_ = input && input.connection.targetConnection;
+ i++;
+ typeBlock = typeBlock.nextConnection &&
+ typeBlock.nextConnection.targetBlock();
+ }
+ },
+ updateShape_: function() {
+ // Modify this block to have the correct number of inputs.
+ // Add new inputs.
+ for (var i = 0; i < this.typeCount_; i++) {
+ if (!this.getInput('TYPE' + i)) {
+ var input = this.appendValueInput('TYPE' + i);
+ if (i == 0) {
+ input.appendField('any of');
+ }
+ }
+ }
+ // Remove deleted inputs.
+ while (this.getInput('TYPE' + i)) {
+ this.removeInput('TYPE' + i);
+ i++;
+ }
+ }
+};
+
+Blockly.Blocks['type_group_container'] = {
+ // Container.
+ init: function() {
+ this.jsonInit({
+ "message0": "add types %1 %2",
+ "args0": [
+ {"type": "input_dummy"},
+ {"type": "input_statement", "name": "STACK"}
+ ],
+ "colour": 230,
+ "tooltip": "Add, or remove allowed type.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677"
+ });
+ }
+};
+
+Blockly.Blocks['type_group_item'] = {
+ // Add type.
+ init: function() {
+ this.jsonInit({
+ "message0": "type",
+ "previousStatement": null,
+ "nextStatement": null,
+ "colour": 230,
+ "tooltip": "Add a new allowed type.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677"
+ });
+ }
+};
+
+Blockly.Blocks['type_null'] = {
+ // Null type.
+ valueType: null,
+ init: function() {
+ this.jsonInit({
+ "message0": "any",
+ "output": "Type",
+ "colour": 230,
+ "tooltip": "Any type is allowed.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602"
+ });
+ }
+};
+
+Blockly.Blocks['type_boolean'] = {
+ // Boolean type.
+ valueType: 'Boolean',
+ init: function() {
+ this.jsonInit({
+ "message0": "Boolean",
+ "output": "Type",
+ "colour": 230,
+ "tooltip": "Booleans (true/false) are allowed.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602"
+ });
+ }
+};
+
+Blockly.Blocks['type_number'] = {
+ // Number type.
+ valueType: 'Number',
+ init: function() {
+ this.jsonInit({
+ "message0": "Number",
+ "output": "Type",
+ "colour": 230,
+ "tooltip": "Numbers (int/float) are allowed.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602"
+ });
+ }
+};
+
+Blockly.Blocks['type_string'] = {
+ // String type.
+ valueType: 'String',
+ init: function() {
+ this.jsonInit({
+ "message0": "String",
+ "output": "Type",
+ "colour": 230,
+ "tooltip": "Strings (text) are allowed.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602"
+ });
+ }
+};
+
+Blockly.Blocks['type_list'] = {
+ // List type.
+ valueType: 'Array',
+ init: function() {
+ this.jsonInit({
+ "message0": "Array",
+ "output": "Type",
+ "colour": 230,
+ "tooltip": "Arrays (lists) are allowed.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602"
+ });
+ }
+};
+
+Blockly.Blocks['type_other'] = {
+ // Other type.
+ init: function() {
+ this.jsonInit({
+ "message0": "other %1",
+ "args0": [{"type": "field_input", "name": "TYPE", "text": ""}],
+ "output": "Type",
+ "colour": 230,
+ "tooltip": "Custom type to allow.",
+ "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=702"
+ });
+ }
+};
+
+Blockly.Blocks['colour_hue'] = {
+ // Set the colour of the block.
+ init: function() {
+ this.appendDummyInput()
+ .appendField('hue:')
+ .appendField(new Blockly.FieldAngle('0', this.validator), 'HUE');
+ this.setOutput(true, 'Colour');
+ this.setTooltip('Paint the block with this colour.');
+ this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=55');
+ },
+ validator: function(text) {
+ // Update the current block's colour to match.
+ var hue = parseInt(text, 10);
+ if (!isNaN(hue)) {
+ this.sourceBlock_.setColour(hue);
+ }
+ },
+ mutationToDom: function(workspace) {
+ var container = document.createElement('mutation');
+ container.setAttribute('colour', this.getColour());
+ return container;
+ },
+ domToMutation: function(container) {
+ this.setColour(container.getAttribute('colour'));
+ }
+};
+
+/**
+ * Check to see if more than one field has this name.
+ * Highly inefficient (On^2), but n is small.
+ * @param {!Blockly.Block} referenceBlock Block to check.
+ */
+function fieldNameCheck(referenceBlock) {
+ if (!referenceBlock.workspace) {
+ // Block has been deleted.
+ return;
+ }
+ var name = referenceBlock.getFieldValue('FIELDNAME').toLowerCase();
+ var count = 0;
+ var blocks = referenceBlock.workspace.getAllBlocks();
+ for (var i = 0, block; block = blocks[i]; i++) {
+ var otherName = block.getFieldValue('FIELDNAME');
+ if (!block.disabled && !block.getInheritedDisabled() &&
+ otherName && otherName.toLowerCase() == name) {
+ count++;
+ }
+ }
+ var msg = (count > 1) ?
+ 'There are ' + count + ' field blocks\n with this name.' : null;
+ referenceBlock.setWarningText(msg);
+}
+
+/**
+ * Check to see if more than one input has this name.
+ * Highly inefficient (On^2), but n is small.
+ * @param {!Blockly.Block} referenceBlock Block to check.
+ */
+function inputNameCheck(referenceBlock) {
+ if (!referenceBlock.workspace) {
+ // Block has been deleted.
+ return;
+ }
+ var name = referenceBlock.getFieldValue('INPUTNAME').toLowerCase();
+ var count = 0;
+ var blocks = referenceBlock.workspace.getAllBlocks();
+ for (var i = 0, block; block = blocks[i]; i++) {
+ var otherName = block.getFieldValue('INPUTNAME');
+ if (!block.disabled && !block.getInheritedDisabled() &&
+ otherName && otherName.toLowerCase() == name) {
+ count++;
+ }
+ }
+ var msg = (count > 1) ?
+ 'There are ' + count + ' input blocks\n with this name.' : null;
+ referenceBlock.setWarningText(msg);
+}
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/io.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/io.js
new file mode 100644
index 000000000..360380da2
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/io.js
@@ -0,0 +1,262 @@
+/**
+ * @license Licensed under the Apache License, Version 2.0 (the "License"):
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/**
+ * @fileoverview Blocks for Arduino Digital and Analogue input and output
+ * functions. The Arduino function syntax can be found at
+ * http://arduino.cc/en/Reference/HomePage
+ *
+ * TODO: maybe change this to a "PIN" BlocklyType
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.io');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+/** Common HSV hue for all blocks in this category. */
+Blockly.Blocks.io.HUE = 250;
+
+Blockly.Blocks['io_digitalwrite'] = {
+ /**
+ * Block for creating a 'set pin' to a state.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/DigitalWrite');
+ this.setColour(Blockly.Blocks.io.HUE);
+ this.appendValueInput('STATE')
+ .appendField(Blockly.Msg.ARD_DIGITALWRITE)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.digitalPins), 'PIN')
+ .appendField(Blockly.Msg.ARD_WRITE_TO)
+ .setCheck(Blockly.Types.BOOLEAN.checkList);
+ this.setInputsInline(false);
+ this.setPreviousStatement(true, null);
+ this.setNextStatement(true, null);
+ this.setTooltip(Blockly.Msg.ARD_DIGITALWRITE_TIP);
+ },
+ /**
+ * Updates the content of the the pin related fields.
+ * @this Blockly.Block
+ */
+ updateFields: function() {
+ Blockly.Arduino.Boards.refreshBlockFieldDropdown(
+ this, 'PIN', 'digitalPins');
+ }
+};
+
+Blockly.Blocks['io_digitalread'] = {
+ /**
+ * Block for creating a 'read pin'.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/DigitalRead');
+ this.setColour(Blockly.Blocks.io.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_DIGITALREAD)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.digitalPins), 'PIN');
+ this.setOutput(true, Blockly.Types.BOOLEAN.output);
+ this.setTooltip(Blockly.Msg.ARD_DIGITALREAD_TIP);
+ },
+ /** @return {!string} The type of return value for the block, an integer. */
+ getBlockType: function() {
+ return Blockly.Types.BOOLEAN;
+ },
+ /**
+ * Updates the content of the the pin related fields.
+ * @this Blockly.Block
+ */
+ updateFields: function() {
+ Blockly.Arduino.Boards.refreshBlockFieldDropdown(
+ this, 'PIN', 'digitalPins');
+ }
+};
+
+Blockly.Blocks['io_builtin_led'] = {
+ /**
+ * Block for setting built-in LED to a state.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/DigitalWrite');
+ this.setColour(Blockly.Blocks.io.HUE);
+ this.appendValueInput('STATE')
+ .appendField(Blockly.Msg.ARD_BUILTIN_LED)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.builtinLed), 'BUILT_IN_LED')
+ .appendField(Blockly.Msg.ARD_WRITE_TO)
+ .setCheck(Blockly.Types.BOOLEAN.checkList);
+ this.setInputsInline(false);
+ this.setPreviousStatement(true, null);
+ this.setNextStatement(true, null);
+ this.setTooltip(Blockly.Msg.ARD_BUILTIN_LED_TIP);
+ },
+ /**
+ * Updates the content of the the pin related fields.
+ * @this Blockly.Block
+ */
+ updateFields: function() {
+ Blockly.Arduino.Boards.refreshBlockFieldDropdown(
+ this, 'BUILT_IN_LED', 'builtinLed');
+ },
+ /** @return {!string} The type of input value for the block, an integer. */
+ getBlockType: function() {
+ return Blockly.Types.BOOLEAN;
+ },
+};
+
+Blockly.Blocks['io_analogwrite'] = {
+ /**
+ * Block for creating a 'set pin' to an analogue value.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/AnalogWrite');
+ this.setColour(Blockly.Blocks.io.HUE);
+ this.appendValueInput('NUM')
+ .appendField(Blockly.Msg.ARD_ANALOGWRITE)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.pwmPins), 'PIN')
+ .appendField(Blockly.Msg.ARD_WRITE_TO)
+ .setCheck(Blockly.Types.NUMBER.output);
+ this.setInputsInline(false);
+ this.setPreviousStatement(true, null);
+ this.setNextStatement(true, null);
+ this.setTooltip(Blockly.Msg.ARD_ANALOGWRITE_TIP);
+ },
+ /**
+ * Updates the content of the the pin related fields.
+ * @this Blockly.Block
+ */
+ updateFields: function() {
+ Blockly.Arduino.Boards.refreshBlockFieldDropdown(this, 'PIN', 'pwmPins');
+ },
+ /** @return {!string} The type of input value for the block, an integer. */
+ getBlockType: function() {
+ return Blockly.Types.NUMBER;
+ },
+};
+
+Blockly.Blocks['io_analogread'] = {
+ /**
+ * Block for reading an analogue input.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/AnalogRead');
+ this.setColour(Blockly.Blocks.io.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_ANALOGREAD)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.analogPins), 'PIN');
+ this.setOutput(true, Blockly.Types.NUMBER.output);
+ this.setTooltip(Blockly.Msg.ARD_ANALOGREAD_TIP);
+ },
+ /** @return {!string} The type of return value for the block, an integer. */
+ getBlockType: function() {
+ return Blockly.Types.NUMBER;
+ },
+ /**
+ * Updates the content of the the pin related fields.
+ * @this Blockly.Block
+ */
+ updateFields: function() {
+ Blockly.Arduino.Boards.refreshBlockFieldDropdown(this, 'PIN', 'analogPins');
+ }
+};
+
+Blockly.Blocks['io_highlow'] = {
+ /**
+ * Block for creating a pin state.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/Constants');
+ this.setColour(Blockly.Blocks.io.HUE);
+ this.appendDummyInput()
+ .appendField(
+ new Blockly.FieldDropdown([[Blockly.Msg.ARD_HIGH, 'HIGH'], [Blockly.Msg.ARD_LOW, 'LOW']]),
+ 'STATE');
+ this.setOutput(true, Blockly.Types.BOOLEAN.output);
+ this.setTooltip(Blockly.Msg.ARD_HIGHLOW_TIP);
+ },
+ /** @return {!string} The type of return value for the block, an integer. */
+ getBlockType: function() {
+ return Blockly.Types.BOOLEAN;
+ }
+};
+
+Blockly.Blocks['io_pulsein'] = {
+ /**
+ * Block for measuring the duration of a pulse in an input pin.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "type": "math_foo",
+ "message0": Blockly.Msg.ARD_PULSE_READ,
+ "args0": [{
+ "type": "input_value",
+ "name": "PULSETYPE",
+ "check": Blockly.Types.BOOLEAN.check
+ }, {
+ "type": "field_dropdown",
+ "name": "PULSEPIN",
+ "options": Blockly.Arduino.Boards.selected.digitalPins
+ }
+ ],
+ "output": Blockly.Types.NUMBER.output,
+ "inputsInline": true,
+ "colour": Blockly.Blocks.io.HUE,
+ "tooltip": Blockly.Msg.ARD_PULSE_TIP,
+ "helpUrl": 'https://www.arduino.cc/en/Reference/PulseIn'
+ });
+ },
+ /** @return {!string} The type of input value for the block, an integer. */
+ getBlockType: function() {
+ return Blockly.Types.NUMBER;
+ }
+};
+
+Blockly.Blocks['io_pulsetimeout'] = {
+ /**
+ * Block for measuring (with a time-out) the duration of a pulse in an input
+ * pin.
+ * @this Blockly.Block
+ */
+ init: function () {
+ this.jsonInit({
+ "type": "math_foo",
+ "message0": Blockly.Msg.ARD_PULSE_READ_TIMEOUT,
+ "args0": [{
+ "type": "input_value",
+ "name": "PULSETYPE",
+ "check": Blockly.Types.BOOLEAN.check
+ }, {
+ "type": "field_dropdown",
+ "name": "PULSEPIN",
+ "options": Blockly.Arduino.Boards.selected.digitalPins
+ }, {
+ "type": "input_value",
+ "name": "TIMEOUT",
+ "check": Blockly.Types.NUMBER.check
+ }
+ ],
+ "output": Blockly.Types.NUMBER.output,
+ "inputsInline": true,
+ "colour": Blockly.Blocks.io.HUE,
+ "tooltip": Blockly.Msg.ARD_PULSETIMEOUT_TIP,
+ "helpUrl": 'https://www.arduino.cc/en/Reference/PulseIn'
+ });
+ },
+ /** @return {!string} The type of input value for the block, an integer. */
+ getBlockType: function() {
+ return Blockly.Types.NUMBER;
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/logo.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/logo.js
new file mode 100644
index 000000000..30dfdc68b
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/logo.js
@@ -0,0 +1,140 @@
+/**
+ * @license Licensed under the Apache License, Version 2.0 (the "License"):
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/**
+ * @fileoverview Blocks use for the Ardublockly logo creation.
+ * These are not mean to used at all.
+ *
+ * Generator:
+ * var noCode = function(block) { return ''; };
+ * Blockly.Arduino['ardublockly_name_bottom'] = noCode;
+ * Blockly.Arduino['ardublockly_plus_top_large'] = noCode;
+ * Blockly.Arduino['ardublockly_plus_top_small'] = noCode;
+ * Blockly.Arduino['ardublockly_plus_bottom_large'] = noCode;
+ * Blockly.Arduino['ardublockly_plus_bottom_small'] = noCode;
+ * Blockly.Arduino['ardublockly_plus_both_small'] = noCode;
+ * Blockly.Arduino['ardublockly_plus_both_large'] = noCode;
+ * Blockly.Arduino['ardublockly_minus_large'] = noCode;
+ * Blockly.Arduino['ardublockly_minus_small'] = noCode;
+ *
+ * Toolbox:
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.logo');
+
+goog.require('Blockly.Blocks');
+
+
+Blockly.Blocks.logo.HUE = 180;
+
+/* Ardublockly block */
+Blockly.Blocks['ardublockly_name_top'] = {
+ init: function() {
+ this.appendDummyInput()
+ .appendField("Ardublockly");
+ this.setPreviousStatement(true);
+ this.setColour(Blockly.Blocks.logo.HUE);
+ }
+};
+
+Blockly.Blocks['ardublockly_name_bottom'] = {
+ init: function() {
+ this.appendDummyInput()
+ .appendField("Ardublockly");
+ this.setNextStatement(true);
+ this.setColour(Blockly.Blocks.logo.HUE);
+ this.setTooltip('');
+ }
+};
+
+/* Plus block */
+Blockly.Blocks['ardublockly_plus_top_large'] = {
+ init: function() {
+ this.appendValueInput("NAME")
+ .appendField(" +");
+ this.setNextStatement(true);
+ this.setColour(Blockly.Blocks.logo.HUE);
+ }
+};
+
+Blockly.Blocks['ardublockly_plus_top_small'] = {
+ init: function() {
+ this.appendValueInput("NAME")
+ .appendField(" +");
+ this.setNextStatement(true);
+ this.setColour(Blockly.Blocks.logo.HUE);
+ }
+};
+
+Blockly.Blocks['ardublockly_plus_bottom_large'] = {
+ init: function() {
+ this.appendValueInput("NAME")
+ .appendField(" +");
+ this.setPreviousStatement(true);
+ this.setColour(Blockly.Blocks.logo.HUE);
+ }
+};
+
+Blockly.Blocks['ardublockly_plus_bottom_small'] = {
+ init: function() {
+ this.appendValueInput("NAME")
+ .appendField(" +");
+ this.setPreviousStatement(true);
+ this.setColour(Blockly.Blocks.logo.HUE);
+ }
+};
+
+Blockly.Blocks['ardublockly_plus_both_small'] = {
+ init: function() {
+ this.appendValueInput("NAME")
+ .appendField(" +");
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setColour(Blockly.Blocks.logo.HUE);
+ }
+};
+
+Blockly.Blocks['ardublockly_plus_both_large'] = {
+ init: function() {
+ this.appendValueInput("NAME")
+ .appendField(" +");
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setColour(Blockly.Blocks.logo.HUE);
+ }
+};
+
+/* Minus block */
+Blockly.Blocks['ardublockly_minus_large'] = {
+ init: function() {
+ this.appendDummyInput()
+ .appendField("- ");
+ this.setInputsInline(false);
+ this.setOutput(true);
+ this.setColour(Blockly.Blocks.logo.HUE);
+ }
+};
+
+Blockly.Blocks['ardublockly_minus_small'] = {
+ init: function() {
+ this.appendDummyInput()
+ .appendField("- ");
+ this.setInputsInline(false);
+ this.setOutput(true);
+ this.setColour(Blockly.Blocks.logo.HUE);
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/map.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/map.js
new file mode 100644
index 000000000..473be0c29
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/map.js
@@ -0,0 +1,48 @@
+/**
+ * @license Licensed under the Apache License, Version 2.0 (the "License"):
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/**
+ * @fileoverview Block for the Arduino map functionality.
+ * The Arduino built in functions syntax can be found at:
+ * http://arduino.cc/en/Reference/HomePage
+ *
+ * TODO: This block can be improved to set the new range properly.
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.map');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/** Common HSV hue for all blocks in this category. */
+Blockly.Blocks.map.HUE = 230;
+
+Blockly.Blocks['base_map'] = {
+ /**
+ * Block for creating a the map function.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/map');
+ this.setColour(Blockly.Blocks.map.HUE);
+ this.appendValueInput('NUM')
+ .appendField(Blockly.Msg.ARD_MAP)
+ .setCheck(Blockly.Types.NUMBER.checkList);
+ this.appendValueInput('DMAX')
+ .appendField(Blockly.Msg.ARD_MAP_VAL)
+ .setCheck(Blockly.Types.NUMBER.checkList);
+ this.appendDummyInput()
+ .appendField(']');
+ this.setInputsInline(true);
+ this.setOutput(true);
+ this.setTooltip(Blockly.Msg.ARD_MAP_TIP);
+ },
+ /** @return {string} The type of return value for the block, an integer. */
+ getBlockType: function() {
+ return Blockly.Types.NUMBER;
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/procedures.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/procedures.js
new file mode 100644
index 000000000..44f245533
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/procedures.js
@@ -0,0 +1,41 @@
+/**
+ * @license Licensed under the Apache License, Version 2.0 (the "License"):
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/**
+ * @fileoverview Block for the Arduino functions.
+ * The Arduino built in functions syntax can be found at:
+ * https://arduino.cc/en/Reference/HomePage
+ */
+'use strict';
+
+goog.require('Blockly.Blocks');
+
+
+/** Common HSV hue for all blocks in this category. */
+Blockly.Blocks.procedures.HUE = 290;
+
+Blockly.Blocks['arduino_functions'] = {
+ /**
+ * Block for defining the Arduino setup() and loop() functions.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_FUN_RUN_SETUP);
+ this.appendStatementInput('SETUP_FUNC');
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_FUN_RUN_LOOP);
+ this.appendStatementInput('LOOP_FUNC');
+ this.setInputsInline(false);
+ this.setColour(Blockly.Blocks.procedures.HUE);
+ this.setTooltip(Blockly.Msg.ARD_FUN_RUN_TIP);
+ this.setHelpUrl('https://arduino.cc/en/Reference/Loop');
+ this.contextMenu = false;
+ },
+ /** @return {!boolean} True if the block instance is in the workspace. */
+ getArduinoLoopsInstance: function() {
+ return true;
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/serial.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/serial.js
new file mode 100644
index 000000000..6413f1dd4
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/serial.js
@@ -0,0 +1,130 @@
+/**
+ * @license Licensed under the Apache License, Version 2.0 (the "License"):
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/**
+ * @fileoverview Blocks for the Arduino serial communication functions.
+ * The Arduino built in functions syntax can be found at:
+ * http://arduino.cc/en/Reference/HomePage
+ *
+ * TODO: There are more function that can be added:
+ * http://arduino.cc/en/Reference/Serial
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.serial');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/** Common HSV hue for all blocks in this category. */
+Blockly.Blocks.serial.HUE = 160;
+
+Blockly.Blocks['serial_setup'] = {
+ /**
+ * Block for setting the speed of the serial connection.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Serial/Begin');
+ this.setColour(Blockly.Blocks.serial.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_SERIAL_SETUP)
+ .appendField(
+ new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.serial), 'SERIAL_ID')
+ .appendField(Blockly.Msg.ARD_SERIAL_SPEED)
+ .appendField(
+ new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.serialSpeed), 'SPEED')
+ .appendField(Blockly.Msg.ARD_SERIAL_BPS);
+ this.setInputsInline(true);
+ this.setTooltip(Blockly.Msg.ARD_SERIAL_SETUP_TIP);
+ },
+ /**
+ * Returns the serial instance name.
+ * @return {!string} Serial instance name.
+ * @this Blockly.Block
+ */
+ getSerialSetupInstance: function() {
+ return this.getFieldValue('SERIAL_ID');
+ },
+ /**
+ * Updates the content of the the serial related fields.
+ * @this Blockly.Block
+ */
+ updateFields: function() {
+ Blockly.Arduino.Boards.refreshBlockFieldDropdown(
+ this, 'SERIAL_ID', 'serial');
+ Blockly.Arduino.Boards.refreshBlockFieldDropdown(
+ this, 'SPEED', 'serialSpeed');
+ }
+};
+
+Blockly.Blocks['serial_print'] = {
+ /**
+ * Block for creating a write to serial com function.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://www.arduino.cc/en/Serial/Print');
+ this.setColour(Blockly.Blocks.serial.HUE);
+ this.appendDummyInput()
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.serial), 'SERIAL_ID')
+ .appendField(Blockly.Msg.ARD_SERIAL_PRINT);
+ this.appendValueInput('CONTENT');
+ this.appendDummyInput()
+ .appendField(new Blockly.FieldCheckbox('TRUE'), 'NEW_LINE')
+ .appendField(Blockly.Msg.ARD_SERIAL_PRINT_NEWLINE);
+ this.setInputsInline(true);
+ this.setPreviousStatement(true, null);
+ this.setNextStatement(true, null);
+ this.setTooltip(Blockly.Msg.ARD_SERIAL_PRINT_TIP);
+ },
+ /**
+ * Called whenever anything on the workspace changes.
+ * It checks the instances of serial_setup and attaches a warning to this
+ * block if not valid data is found.
+ * @this Blockly.Block
+ */
+ onchange: function(event) {
+ if (!this.workspace || event.type == Blockly.Events.MOVE ||
+ event.type == Blockly.Events.UI) {
+ return; // Block deleted or irrelevant event
+ }
+
+ // Get the Serial instance from this block
+ var thisInstanceName = this.getFieldValue('SERIAL_ID');
+ // Iterate through top level blocks to find setup instance for the serial id
+ var blocks = Blockly.mainWorkspace.getTopBlocks();
+ var setupInstancePresent = false;
+ for (var x = 0; x < blocks.length; x++) {
+ var func = blocks[x].getSerialSetupInstance;
+ if (func) {
+ var setupBlockInstanceName = func.call(blocks[x]);
+ if (thisInstanceName == setupBlockInstanceName) {
+ setupInstancePresent = true;
+ break;
+ }
+ }
+ }
+
+ if (!setupInstancePresent) {
+ this.setWarningText(Blockly.Msg.ARD_SERIAL_PRINT_WARN.replace('%1',
+ thisInstanceName), 'serial_setup');
+ } else {
+ this.setWarningText(null, 'serial_setup');
+ }
+ },
+ /**
+ * Updates the content of the the serial related fields.
+ * @this Blockly.Block
+ */
+ updateFields: function() {
+ Blockly.Arduino.Boards.refreshBlockFieldDropdown(
+ this, 'SERIAL_ID', 'serial');
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/servo.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/servo.js
new file mode 100644
index 000000000..ca56e197e
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/servo.js
@@ -0,0 +1,84 @@
+/**
+ * @license Licensed under the Apache License, Version 2.0 (the "License"):
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/**
+ * @fileoverview Arduino blocks for the Servo library.
+ * The Arduino Servo functions can be found in
+ * http://arduino.cc/en/reference/servo
+ *
+ * TODO: Add angle selector instead of block input.
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.servo');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/** Common HSV hue for all blocks in this category. */
+Blockly.Blocks.servo.HUE = 60;
+
+Blockly.Blocks['servo_write'] = {
+ /**
+ * Block for writing an angle value into a servo pin.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/ServoWrite');
+ this.setColour(Blockly.Blocks.servo.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_SERVO_WRITE)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.digitalPins), 'SERVO_PIN');
+ this.setInputsInline(false);
+ this.appendValueInput('SERVO_ANGLE')
+ .setCheck(Blockly.Types.NUMBER.checkList)
+ .appendField(Blockly.Msg.ARD_SERVO_WRITE_TO);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_SERVO_WRITE_DEG_180);
+ this.setInputsInline(true);
+ this.setPreviousStatement(true, null);
+ this.setNextStatement(true, null);
+ this.setTooltip(Blockly.Msg.ARD_SERVO_WRITE_TIP);
+ },
+ /**
+ * Updates the content of the the pin related fields.
+ * @this Blockly.Block
+ */
+ updateFields: function() {
+ Blockly.Arduino.Boards.refreshBlockFieldDropdown(
+ this, 'SERVO_PIN', 'digitalPins');
+ }
+};
+
+Blockly.Blocks['servo_read'] = {
+ /**
+ * Block for reading an angle value of a servo pin.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/ServoRead');
+ this.setColour(Blockly.Blocks.servo.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_SERVO_READ)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.digitalPins), 'SERVO_PIN');
+ this.setOutput(true, Blockly.Types.NUMBER.output);
+ this.setTooltip(Blockly.Msg.ARD_SERVO_READ_TIP);
+ },
+ /** @return {string} The type of return value for the block, an integer. */
+ getBlockType: function() {
+ return Blockly.Types.NUMBER;
+ },
+ /**
+ * Updates the content of the the pin related fields.
+ * @this Blockly.Block
+ */
+ updateFields: function() {
+ Blockly.Arduino.Boards.refreshBlockFieldDropdown(
+ this, 'SERVO_PIN', 'digitalPins');
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/spi.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/spi.js
new file mode 100644
index 000000000..e95cb53f6
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/spi.js
@@ -0,0 +1,211 @@
+ /**
+ * @license Licensed under the Apache License, Version 2.0 (the "License"):
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/**
+ * @fileoverview Blocks for Arduino SPI library.
+ * The Arduino SPI functions syntax can be found in:
+ * http://arduino.cc/en/Reference/SPI
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.spi');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/** Common HSV hue for all blocks in this category. */
+Blockly.Blocks.spi.HUE = 170;
+
+Blockly.Blocks['spi_setup'] = {
+ /**
+ * Block for the spi configuration. Info in the setHelpUrl link.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/SPI');
+ this.setColour(Blockly.Blocks.spi.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_SPI_SETUP)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.spi), 'SPI_ID')
+ .appendField(Blockly.Msg.ARD_SPI_SETUP_CONF);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_SPI_SETUP_SHIFT)
+ .appendField(
+ new Blockly.FieldDropdown(
+ [[Blockly.Msg.ARD_SPI_SETUP_MSBFIRST, 'MSBFIRST'],
+ [Blockly.Msg.ARD_SPI_SETUP_LSBFIRST, 'LSBFIRST']]),
+ 'SPI_SHIFT_ORDER');
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_SPI_SETUP_DIVIDE)
+ .appendField(
+ new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.spiClockDivide),
+ 'SPI_CLOCK_DIVIDE');
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_SPI_SETUP_MODE)
+ .appendField(
+ new Blockly.FieldDropdown(
+ [[Blockly.Msg.ARD_SPI_SETUP_MODE0, 'SPI_MODE0'],
+ [Blockly.Msg.ARD_SPI_SETUP_MODE1, 'SPI_MODE1'],
+ [Blockly.Msg.ARD_SPI_SETUP_MODE2, 'SPI_MODE2'],
+ [Blockly.Msg.ARD_SPI_SETUP_MODE3, 'SPI_MODE3']]),
+ 'SPI_MODE');
+ this.setTooltip(Blockly.Msg.ARD_SPI_SETUP_TIP);
+ },
+ /**
+ * Returns the selected SPI instance.
+ * @return {!string} SPI instance name.
+ * @this Blockly.Block
+ */
+ getSpiSetupInstance: function() {
+ return this.getFieldValue('SPI_ID');
+ },
+ /**
+ * Updates the content of the the board SPI related fields.
+ * @this Blockly.Block
+ */
+ updateFields: function() {
+ Blockly.Arduino.Boards.refreshBlockFieldDropdown(
+ this, 'SPI_ID', 'spi');
+ Blockly.Arduino.Boards.refreshBlockFieldDropdown(
+ this, 'SPI_CLOCK_DIVIDE', 'spiClockDivide');
+ }
+};
+
+Blockly.Blocks['spi_transfer'] = {
+ /**
+ * Block for for the spi transfer. Info in the setHelpUrl link.
+ * @this Blockly.Block
+ */
+ init: function() {
+ // Drop down list to contain all digital pins plus an option for 'none'
+ var slaveNone = [[Blockly.Msg.ARD_SPI_TRANS_NONE, 'none']];
+ var digitalPinsExtended = slaveNone.concat(
+ Blockly.Arduino.Boards.selected.digitalPins);
+
+ this.setHelpUrl('http://arduino.cc/en/Reference/SPITransfer');
+ this.setColour(Blockly.Blocks.spi.HUE);
+ this.appendDummyInput()
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.spi), 'SPI_ID');
+ this.appendValueInput('SPI_DATA')
+ .setCheck(Blockly.Types.NUMBER.checkList)
+ .appendField(Blockly.Msg.ARD_SPI_TRANS_VAL);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_SPI_TRANS_SLAVE)
+ .appendField(
+ new Blockly.FieldDropdown(digitalPinsExtended), 'SPI_SS');
+ this.setInputsInline(true);
+ this.setPreviousStatement(true, null);
+ this.setNextStatement(true, null);
+ this.setTooltip(Blockly.Msg.ARD_SPI_TRANS_TIP);
+ },
+ /**
+ * Called whenever anything on the workspace changes.
+ * It checks the instances of stepper_config and attaches a warning to this
+ * block if not valid data is found.
+ * @this Blockly.Block
+ */
+ onchange: function(event) {
+ if (!this.workspace || event.type == Blockly.Events.MOVE ||
+ event.type == Blockly.Events.UI) {
+ return; // Block deleted or irrelevant event
+ }
+
+ // Get the Serial instance from this block
+ var thisInstanceName = this.getFieldValue('SPI_ID');
+
+ // Iterate through top level blocks to find a setup instance for the SPI id
+ var blocks = Blockly.mainWorkspace.getTopBlocks();
+ var setupInstancePresent = false;
+ for (var x = 0, length_ = blocks.length; x < length_; x++) {
+ var func = blocks[x].getSpiSetupInstance;
+ if (func) {
+ var setupBlockInstanceName = func.call(blocks[x]);
+ if (thisInstanceName == setupBlockInstanceName) {
+ setupInstancePresent = true;
+ }
+ }
+ }
+
+ if (!setupInstancePresent) {
+ this.setWarningText(
+ Blockly.Msg.ARD_SPI_TRANS_WARN1.replace('%1', thisInstanceName),
+ 'spi_setup');
+ } else {
+ this.setWarningText(null, 'spi_setup');
+ }
+ },
+ /**
+ * Retrieves the type of the selected variable, Arduino code returns a byte,
+ * for now set it to integer.
+ * @return {!string} Blockly type.
+ */
+ getBlockType: function() {
+ return Blockly.Types.NUMBER;
+ },
+ /**
+ * Updates the content of the board SPI related fields.
+ * @this Blockly.Block
+ */
+ updateFields: function() {
+ // Special case, otherwise Blockly.Arduino.Boards.refreshBlockFieldDropdown
+ var field = this.getField('SPI_SS');
+ var fieldValue = field.getValue();
+ var slaveNone = [[Blockly.Msg.ARD_SPI_TRANS_NONE, 'none']];
+ field.menuGenerator_ =
+ slaveNone.concat(Blockly.Arduino.Boards.selected['digitalPins']);
+
+ var currentValuePresent = false;
+ for (var i = 0, length_ = field.menuGenerator_.length; i < length_; i++) {
+ if (fieldValue == field.menuGenerator_[i][1]) {
+ currentValuePresent = true;
+ }
+ }
+ // If the old value is not present any more, add a warning to the block.
+ if (!currentValuePresent) {
+ this.setWarningText(
+ Blockly.Msg.ARD_SPI_TRANS_WARN2.replace('%1', fieldValue), 'bPin');
+ } else {
+ this.setWarningText(null, 'bPin');
+ }
+ }
+};
+
+Blockly.Blocks['spi_transfer_return'] = {
+ /**
+ * Block for for the spi transfer with a return value.
+ * @this Blockly.Block
+ */
+ init: function() {
+ // Drop down list to contain all digital pins plus an option for 'none'
+ var slaveNone = [[Blockly.Msg.ARD_SPI_TRANS_NONE, 'none']];
+ var digitalPinsExtended = slaveNone.concat(
+ Blockly.Arduino.Boards.selected.digitalPins);
+
+ this.setHelpUrl('http://arduino.cc/en/Reference/SPITransfer');
+ this.setColour(Blockly.Blocks.spi.HUE);
+ this.appendDummyInput()
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.spi), 'SPI_ID');
+ this.appendValueInput('SPI_DATA')
+ .appendField(Blockly.Msg.ARD_SPI_TRANS_VAL);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_SPI_TRANS_SLAVE)
+ .appendField(
+ new Blockly.FieldDropdown(digitalPinsExtended), 'SPI_SS');
+ this.setInputsInline(true);
+ this.setOutput(true);
+ this.setTooltip(Blockly.Msg.ARD_SPI_TRANSRETURN_TIP);
+ },
+ /** Same as spi_transfer block */
+ onchange: Blockly.Blocks['spi_transfer'].onchange,
+ /** Same as spi_transfer block */
+ getBlockType: Blockly.Blocks['spi_transfer'].getBlockType,
+ /** Same as spi_transfer block */
+ updateFields: Blockly.Blocks['spi_transfer'].updateFields
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/stepper.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/stepper.js
new file mode 100644
index 000000000..be8ac2c76
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/stepper.js
@@ -0,0 +1,181 @@
+/**
+ * @license Licensed under the Apache License, Version 2.0 (the "License"):
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/**
+ * @fileoverview Blocks for Arduino Stepper library.
+ * The Arduino Servo functions syntax can be found in the following URL:
+ * http://arduino.cc/en/Reference/Stepper
+ * Note that this block uses the Blockly.FieldInstance instead of
+ * Blockly.FieldDropdown which generates a unique instance per setup block
+ * in the workspace.
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.stepper');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/** Common HSV hue for all blocks in this category. */
+Blockly.Blocks.stepper.HUE = 80;
+
+Blockly.Blocks['stepper_config'] = {
+ /**
+ * Block for for the stepper generator configuration including creating
+ * an object instance and setting up the speed. Info in the setHelpUrl link.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var dropdownOptions = [[Blockly.Msg.ARD_STEPPER_TWO_PINS, 'TWO'],
+ [Blockly.Msg.ARD_STEPPER_FOUR_PINS, 'FOUR']];
+ var dropdown = new Blockly.FieldDropdown(dropdownOptions, function(option) {
+ var input = (option == 'FOUR');
+ this.sourceBlock_.updateShape_(input);
+ });
+
+ this.setHelpUrl('http://arduino.cc/en/Reference/StepperConstructor');
+ this.setColour(Blockly.Blocks.stepper.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_STEPPER_SETUP)
+ .appendField(
+ new Blockly.FieldInstance('Stepper',
+ Blockly.Msg.ARD_STEPPER_DEFAULT_NAME,
+ true, true, false),
+ 'STEPPER_NAME')
+ .appendField(Blockly.Msg.ARD_STEPPER_MOTOR);
+ this.appendDummyInput('PINS_DROPDOWN')
+ .setAlign(Blockly.ALIGN_RIGHT)
+ .appendField(Blockly.Msg.ARD_STEPPER_NUMBER_OF_PINS)
+ .appendField(dropdown, "STEPPER_NUMBER_OF_PINS");
+ this.appendDummyInput('PINS')
+ .setAlign(Blockly.ALIGN_RIGHT)
+ .appendField(Blockly.Msg.ARD_STEPPER_PIN1)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.digitalPins), 'STEPPER_PIN1')
+ .appendField(Blockly.Msg.ARD_STEPPER_PIN2)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.digitalPins), 'STEPPER_PIN2');
+ this.appendValueInput('STEPPER_STEPS')
+ .setCheck(Blockly.Types.NUMBER.checkList)
+ .setAlign(Blockly.ALIGN_RIGHT)
+ .appendField(Blockly.Msg.ARD_STEPPER_REVOLVS);
+ this.appendValueInput('STEPPER_SPEED')
+ .setCheck(Blockly.Types.NUMBER.checkList)
+ .setAlign(Blockly.ALIGN_RIGHT)
+ .appendField(Blockly.Msg.ARD_STEPPER_SPEED);
+ this.setTooltip(Blockly.Msg.ARD_STEPPER_SETUP_TIP);
+ },
+ /**
+ * Parse XML to restore the number of pins available.
+ * @param {!Element} xmlElement XML storage element.
+ * @this Blockly.Block
+ */
+ domToMutation: function(xmlElement) {
+ var input = (xmlElement.getAttribute('number_of_pins') == 'FOUR');
+ this.updateShape_(input);
+ },
+ /**
+ * Create XML to represent number of pins selection.
+ * @return {!Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ var input = this.getFieldValue('STEPPER_NUMBER_OF_PINS');
+ container.setAttribute("number_of_pins", input);
+ return container;
+ },
+ /**
+ * Modify this block to have the correct number of pins available.
+ * @param {boolean} fourPins True if this block has a 4 or 2 stepper pins.
+ * @private
+ * @this Blockly.Block
+ */
+ updateShape_: function(fourPins) {
+ // Single check as Pin 3 and 4 should always be added or removed together
+ var extraPinsExist = this.getFieldValue('STEPPER_PIN3');
+ if (fourPins) {
+ if (!extraPinsExist) {
+ this.getInput("PINS")
+ .appendField(Blockly.Msg.ARD_STEPPER_PIN3, "PIN3")
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.digitalPins), 'STEPPER_PIN3')
+ .appendField(Blockly.Msg.ARD_STEPPER_PIN4, "PIN4")
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.digitalPins), 'STEPPER_PIN4');
+ }
+ } else {
+ // Two pins is selected
+ if (extraPinsExist) {
+ this.getInput("PINS").removeField("STEPPER_PIN3");
+ this.getInput("PINS").removeField("PIN3");
+ this.getInput("PINS").removeField("STEPPER_PIN4");
+ this.getInput("PINS").removeField("PIN4");
+ }
+ }
+ },
+ /**
+ * Updates the content of the the pin related fields.
+ * @this Blockly.Block
+ */
+ updateFields: function() {
+ Blockly.Boards.refreshBlockFieldDropdown(
+ this, 'STEPPER_PIN1', 'digitalPins');
+ Blockly.Boards.refreshBlockFieldDropdown(
+ this, 'STEPPER_PIN2', 'digitalPins');
+ Blockly.Boards.refreshBlockFieldDropdown(
+ this, 'STEPPER_PIN3', 'digitalPins');
+ Blockly.Boards.refreshBlockFieldDropdown(
+ this, 'STEPPER_PIN4', 'digitalPins');
+ }
+};
+
+Blockly.Blocks['stepper_step'] = {
+ /**
+ * Block for for the stepper 'step()' function.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/StepperStep');
+ this.setColour(Blockly.Blocks.stepper.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_STEPPER_STEP)
+ .appendField(
+ new Blockly.FieldInstance('Stepper',
+ Blockly.Msg.ARD_STEPPER_DEFAULT_NAME,
+ false, true, false),
+ 'STEPPER_NAME');
+ this.appendValueInput('STEPPER_STEPS')
+ .setCheck(Blockly.Types.NUMBER.checkList);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_STEPPER_STEPS);
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setTooltip(Blockly.Msg.ARD_STEPPER_STEP_TIP);
+ },
+ /**
+ * Called whenever anything on the workspace changes.
+ * It checks/warns if the selected stepper instance has a config block.
+ * @this Blockly.Block
+ */
+ onchange: function(event) {
+ if (!this.workspace || event.type == Blockly.Events.MOVE ||
+ event.type == Blockly.Events.UI) {
+ return; // Block deleted or irrelevant event
+ }
+
+ var instanceName = this.getFieldValue('STEPPER_NAME')
+ if (Blockly.Instances.isInstancePresent(instanceName, 'Stepper', this)) {
+ this.setWarningText(null);
+ } else {
+ // Set a warning to select a valid stepper config block
+ this.setWarningText(
+ Blockly.Msg.ARD_COMPONENT_WARN1.replace(
+ '%1', Blockly.Msg.ARD_STEPPER_COMPONENT).replace(
+ '%2', instanceName));
+ }
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/time.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/time.js
new file mode 100644
index 000000000..a111e65eb
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/time.js
@@ -0,0 +1,117 @@
+/**
+ * @license Licensed under the Apache License, Version 2.0 (the "License"):
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/**
+ * @fileoverview Blocks for Arduino Time functions.
+ * The arduino built in functions syntax can be found in
+ * http://arduino.cc/en/Reference/HomePage
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.time');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/** Common HSV hue for all blocks in this category. */
+Blockly.Blocks.time.HUE = 140;
+
+Blockly.Blocks['time_delay'] = {
+ /**
+ * Delay block definition
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/Delay');
+ this.setColour(Blockly.Blocks.time.HUE);
+ this.appendValueInput('DELAY_TIME_MILI')
+ .setCheck(Blockly.Types.NUMBER.checkList)
+ .appendField(Blockly.Msg.ARD_TIME_DELAY);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_TIME_MS);
+ this.setInputsInline(true);
+ this.setPreviousStatement(true, null);
+ this.setNextStatement(true, null);
+ this.setTooltip(Blockly.Msg.ARD_TIME_DELAY_TIP);
+ }
+};
+
+Blockly.Blocks['time_delaymicros'] = {
+ /**
+ * delayMicroseconds block definition
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/DelayMicroseconds');
+ this.setColour(Blockly.Blocks.time.HUE);
+ this.appendValueInput('DELAY_TIME_MICRO')
+ .setCheck(Blockly.Types.NUMBER.checkList)
+ .appendField(Blockly.Msg.ARD_TIME_DELAY);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_TIME_DELAY_MICROS);
+ this.setInputsInline(true);
+ this.setPreviousStatement(true, null);
+ this.setNextStatement(true, null);
+ this.setTooltip(Blockly.Msg.ARD_TIME_DELAY_MICRO_TIP);
+ }
+};
+
+Blockly.Blocks['time_millis'] = {
+ /**
+ * Elapsed time in milliseconds block definition
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/Millis');
+ this.setColour(Blockly.Blocks.time.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_TIME_MILLIS);
+ this.setOutput(true, Blockly.Types.LARGE_NUMBER.output);
+ this.setTooltip(Blockly.Msg.ARD_TIME_MILLIS_TIP);
+ },
+ /** @return {string} The type of return value for the block, an integer. */
+ getBlockType: function() {
+ return Blockly.Types.LARGE_NUMBER;
+ }
+};
+
+Blockly.Blocks['time_micros'] = {
+ /**
+ * Elapsed time in microseconds block definition
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/Micros');
+ this.setColour(Blockly.Blocks.time.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_TIME_MICROS);
+ this.setOutput(true, Blockly.Types.LARGE_NUMBER.output);
+ this.setTooltip(Blockly.Msg.ARD_TIME_MICROS_TIP);
+ },
+ /**
+ * Should be a long (32bit), but for for now an int.
+ * @return {string} The type of return value for the block, an integer.
+ */
+ getBlockType: function() {
+ return Blockly.Types.LARGE_NUMBER;
+ }
+};
+
+Blockly.Blocks['infinite_loop'] = {
+ /**
+ * Waits forever, end of program.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('');
+ this.setColour(Blockly.Blocks.time.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_TIME_INF);
+ this.setInputsInline(true);
+ this.setPreviousStatement(true);
+ this.setTooltip(Blockly.Msg.ARD_TIME_INF_TIP);
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/tone.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/tone.js
new file mode 100644
index 000000000..8d0982ead
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/tone.js
@@ -0,0 +1,78 @@
+/**
+ * @license Licensed under the Apache License, Version 2.0 (the "License"):
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/**
+ * @fileoverview Blocks for Arduino Tone generation
+ * The Arduino function syntax can be found at
+ * https://www.arduino.cc/en/Reference/tone
+ *
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.tone');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+/** Common HSV hue for all blocks in this category. */
+Blockly.Blocks.tone.HUE = 250;
+
+Blockly.Blocks['io_tone'] = {
+ init: function() {
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_SETTONE)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.digitalPins), "TONEPIN");
+ this.appendValueInput("FREQUENCY")
+ .setCheck(Blockly.Types.NUMBER.checkList)
+ .appendField(Blockly.Msg.ARD_TONEFREQ);
+ this.setInputsInline(true);
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setColour(Blockly.Blocks.tone.HUE);
+ this.setTooltip(Blockly.Msg.ARD_TONE_TIP);
+ this.setHelpUrl('https://www.arduino.cc/en/Reference/tone');
+ },
+ /**
+ * Called whenever anything on the workspace changes.
+ * It checks frequency values and sets a warning if out of range.
+ * @this Blockly.Block
+ */
+ onchange: function(event) {
+ if (!this.workspace || event.type == Blockly.Events.MOVE ||
+ event.type == Blockly.Events.UI) {
+ return; // Block deleted or irrelevant event
+ }
+ var freq = Blockly.Arduino.valueToCode(
+ this, "FREQUENCY", Blockly.Arduino.ORDER_ATOMIC)
+ if (freq < 31 || freq > 65535) {
+ this.setWarningText(Blockly.Msg.ARD_TONE_WARNING, 'io_tone');
+ } else {
+ this.setWarningText(null, 'io_tone');
+ }
+ },
+ /** @return {!string} The type of input value for the block, an integer. */
+ getBlockType: function() {
+ return Blockly.Types.NUMBER;
+ }
+};
+
+Blockly.Blocks['io_notone'] = {
+ init: function() {
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_NOTONE)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Arduino.Boards.selected.digitalPins), "TONEPIN");
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setColour(Blockly.Blocks.tone.HUE);
+ this.setTooltip(Blockly.Msg.ARD_NOTONE_TIP);
+ this.setHelpUrl('https://www.arduino.cc/en/Reference/noTone');
+ },
+ /** @return {!string} The type of input value for the block, an integer. */
+ getBlockType: function() {
+ return Blockly.Types.NUMBER;
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/variables.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/variables.js
new file mode 100644
index 000000000..c33bf0411
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/arduino/variables.js
@@ -0,0 +1,49 @@
+/**
+ * @license Licensed under the Apache License, Version 2.0 (the "License"):
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/**
+ * @fileoverview Block for the Arduino map functionality.
+ * The Arduino built in functions syntax can be found at:
+ * http://arduino.cc/en/Reference/HomePage
+ *
+ * TODO: This block can be improved to set the new range properly.
+ */
+'use strict';
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/** Common HSV hue for all blocks in this category. */
+Blockly.Blocks.variables.HUE = 330;
+
+Blockly.Blocks['variables_set_type'] = {
+ /**
+ * Block for variable casting.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl('http://arduino.cc/en/Reference/HomePage');
+ this.setColour(Blockly.Blocks.variables.HUE);
+ this.appendValueInput('VARIABLE_SETTYPE_INPUT');
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.ARD_VAR_AS)
+ .appendField(new Blockly.FieldDropdown(
+ Blockly.Types.getValidTypeArray()),
+ 'VARIABLE_SETTYPE_TYPE');
+ this.setInputsInline(true);
+ this.setOutput(true);
+ this.setTooltip(Blockly.Msg.ARD_VAR_AS_TIP);
+ },
+ /**
+ * Assigns a type to the block based on the selected type to cast.
+ * @return {!string} Blockly type for this block configuration.
+ * @this Blockly.Block
+ */
+ getBlockType: function() {
+ var blocklyTypeKey = this.getFieldValue('VARIABLE_SETTYPE_TYPE');
+ return Blockly.Types[blocklyTypeKey];
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/colour.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/colour.js
new file mode 100644
index 000000000..0d89dd5e3
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/colour.js
@@ -0,0 +1,134 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2012 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Colour blocks for Blockly.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.colour');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/**
+ * Common HSV hue for all blocks in this category.
+ */
+Blockly.Blocks.colour.HUE = 20;
+
+Blockly.Blocks['colour_picker'] = {
+ /**
+ * Block for colour picker.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": "%1",
+ "args0": [
+ {
+ "type": "field_colour",
+ "name": "COLOUR",
+ "colour": "#ff0000"
+ }
+ ],
+ "output": "Colour",
+ "colour": Blockly.Blocks.colour.HUE,
+ "helpUrl": Blockly.Msg.COLOUR_PICKER_HELPURL
+ });
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ // Colour block is trivial. Use tooltip of parent block if it exists.
+ this.setTooltip(function() {
+ var parent = thisBlock.getParent();
+ return (parent && parent.getInputsInline() && parent.tooltip) ||
+ Blockly.Msg.COLOUR_PICKER_TOOLTIP;
+ });
+ }
+};
+
+Blockly.Blocks['colour_random'] = {
+ /**
+ * Block for random colour.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.COLOUR_RANDOM_TITLE,
+ "output": "Colour",
+ "colour": Blockly.Blocks.colour.HUE,
+ "tooltip": Blockly.Msg.COLOUR_RANDOM_TOOLTIP,
+ "helpUrl": Blockly.Msg.COLOUR_RANDOM_HELPURL
+ });
+ }
+};
+
+Blockly.Blocks['colour_rgb'] = {
+ /**
+ * Block for composing a colour from RGB components.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl(Blockly.Msg.COLOUR_RGB_HELPURL);
+ this.setColour(Blockly.Blocks.colour.HUE);
+ this.appendValueInput('RED')
+ .setCheck(Blockly.Types.NUMBER.checkList)
+ .setAlign(Blockly.ALIGN_RIGHT)
+ .appendField(Blockly.Msg.COLOUR_RGB_TITLE)
+ .appendField(Blockly.Msg.COLOUR_RGB_RED);
+ this.appendValueInput('GREEN')
+ .setCheck(Blockly.Types.NUMBER.checkList)
+ .setAlign(Blockly.ALIGN_RIGHT)
+ .appendField(Blockly.Msg.COLOUR_RGB_GREEN);
+ this.appendValueInput('BLUE')
+ .setCheck(Blockly.Types.NUMBER.checkList)
+ .setAlign(Blockly.ALIGN_RIGHT)
+ .appendField(Blockly.Msg.COLOUR_RGB_BLUE);
+ this.setOutput(true, 'Colour');
+ this.setTooltip(Blockly.Msg.COLOUR_RGB_TOOLTIP);
+ }
+};
+
+Blockly.Blocks['colour_blend'] = {
+ /**
+ * Block for blending two colours together.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl(Blockly.Msg.COLOUR_BLEND_HELPURL);
+ this.setColour(Blockly.Blocks.colour.HUE);
+ this.appendValueInput('COLOUR1')
+ .setCheck('Colour')
+ .setAlign(Blockly.ALIGN_RIGHT)
+ .appendField(Blockly.Msg.COLOUR_BLEND_TITLE)
+ .appendField(Blockly.Msg.COLOUR_BLEND_COLOUR1);
+ this.appendValueInput('COLOUR2')
+ .setCheck('Colour')
+ .setAlign(Blockly.ALIGN_RIGHT)
+ .appendField(Blockly.Msg.COLOUR_BLEND_COLOUR2);
+ this.appendValueInput('RATIO')
+ .setCheck(Blockly.Types.NUMBER.checkList)
+ .setAlign(Blockly.ALIGN_RIGHT)
+ .appendField(Blockly.Msg.COLOUR_BLEND_RATIO);
+ this.setOutput(true, 'Colour');
+ this.setTooltip(Blockly.Msg.COLOUR_BLEND_TOOLTIP);
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/lists.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/lists.js
new file mode 100644
index 000000000..5490eca45
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/lists.js
@@ -0,0 +1,754 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2012 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview List blocks for Blockly.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.lists');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/**
+ * Common HSV hue for all blocks in this category.
+ */
+Blockly.Blocks.lists.HUE = 260;
+
+Blockly.Blocks['lists_create_empty'] = {
+ /**
+ * Block for creating an empty list.
+ * The 'list_create_with' block is preferred as it is more flexible.
+ *
+ *
+ *
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.LISTS_CREATE_EMPTY_TITLE,
+ "output": "Array",
+ "colour": Blockly.Blocks.lists.HUE,
+ "tooltip": Blockly.Msg.LISTS_CREATE_EMPTY_TOOLTIP,
+ "helpUrl": Blockly.Msg.LISTS_CREATE_EMPTY_HELPURL
+ });
+ }
+};
+
+Blockly.Blocks['lists_create_with'] = {
+ /**
+ * Block for creating a list with any number of elements of any type.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl(Blockly.Msg.LISTS_CREATE_WITH_HELPURL);
+ this.setColour(Blockly.Blocks.lists.HUE);
+ this.itemCount_ = 3;
+ this.updateShape_();
+ this.setOutput(true, 'Array');
+ this.setMutator(new Blockly.Mutator(['lists_create_with_item']));
+ this.setTooltip(Blockly.Msg.LISTS_CREATE_WITH_TOOLTIP);
+ },
+ /**
+ * Create XML to represent list inputs.
+ * @return {!Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ container.setAttribute('items', this.itemCount_);
+ return container;
+ },
+ /**
+ * Parse XML to restore the list inputs.
+ * @param {!Element} xmlElement XML storage element.
+ * @this Blockly.Block
+ */
+ domToMutation: function(xmlElement) {
+ this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10);
+ this.updateShape_();
+ },
+ /**
+ * Populate the mutator's dialog with this block's components.
+ * @param {!Blockly.Workspace} workspace Mutator's workspace.
+ * @return {!Blockly.Block} Root block in mutator.
+ * @this Blockly.Block
+ */
+ decompose: function(workspace) {
+ var containerBlock = workspace.newBlock('lists_create_with_container');
+ containerBlock.initSvg();
+ var connection = containerBlock.getInput('STACK').connection;
+ for (var i = 0; i < this.itemCount_; i++) {
+ var itemBlock = workspace.newBlock('lists_create_with_item');
+ itemBlock.initSvg();
+ connection.connect(itemBlock.previousConnection);
+ connection = itemBlock.nextConnection;
+ }
+ return containerBlock;
+ },
+ /**
+ * Reconfigure this block based on the mutator dialog's components.
+ * @param {!Blockly.Block} containerBlock Root block in mutator.
+ * @this Blockly.Block
+ */
+ compose: function(containerBlock) {
+ var itemBlock = containerBlock.getInputTargetBlock('STACK');
+ // Count number of inputs.
+ var connections = [];
+ while (itemBlock) {
+ connections.push(itemBlock.valueConnection_);
+ itemBlock = itemBlock.nextConnection &&
+ itemBlock.nextConnection.targetBlock();
+ }
+ // Disconnect any children that don't belong.
+ for (var i = 0; i < this.itemCount_; i++) {
+ var connection = this.getInput('ADD' + i).connection.targetConnection;
+ if (connection && connections.indexOf(connection) == -1) {
+ connection.disconnect();
+ }
+ }
+ this.itemCount_ = connections.length;
+ this.updateShape_();
+ // Reconnect any child blocks.
+ for (var i = 0; i < this.itemCount_; i++) {
+ Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i);
+ }
+ },
+ /**
+ * Store pointers to any connected child blocks.
+ * @param {!Blockly.Block} containerBlock Root block in mutator.
+ * @this Blockly.Block
+ */
+ saveConnections: function(containerBlock) {
+ var itemBlock = containerBlock.getInputTargetBlock('STACK');
+ var i = 0;
+ while (itemBlock) {
+ var input = this.getInput('ADD' + i);
+ itemBlock.valueConnection_ = input && input.connection.targetConnection;
+ i++;
+ itemBlock = itemBlock.nextConnection &&
+ itemBlock.nextConnection.targetBlock();
+ }
+ },
+ /**
+ * Modify this block to have the correct number of inputs.
+ * @private
+ * @this Blockly.Block
+ */
+ updateShape_: function() {
+ if (this.itemCount_ && this.getInput('EMPTY')) {
+ this.removeInput('EMPTY');
+ } else if (!this.itemCount_ && !this.getInput('EMPTY')) {
+ this.appendDummyInput('EMPTY')
+ .appendField(Blockly.Msg.LISTS_CREATE_EMPTY_TITLE);
+ }
+ // Add new inputs.
+ for (var i = 0; i < this.itemCount_; i++) {
+ if (!this.getInput('ADD' + i)) {
+ var input = this.appendValueInput('ADD' + i);
+ if (i == 0) {
+ input.appendField(Blockly.Msg.LISTS_CREATE_WITH_INPUT_WITH);
+ }
+ }
+ }
+ // Remove deleted inputs.
+ while (this.getInput('ADD' + i)) {
+ this.removeInput('ADD' + i);
+ i++;
+ }
+ }
+};
+
+Blockly.Blocks['lists_create_with_container'] = {
+ /**
+ * Mutator block for list container.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setColour(Blockly.Blocks.lists.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.LISTS_CREATE_WITH_CONTAINER_TITLE_ADD);
+ this.appendStatementInput('STACK');
+ this.setTooltip(Blockly.Msg.LISTS_CREATE_WITH_CONTAINER_TOOLTIP);
+ this.contextMenu = false;
+ }
+};
+
+Blockly.Blocks['lists_create_with_item'] = {
+ /**
+ * Mutator bolck for adding items.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setColour(Blockly.Blocks.lists.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.LISTS_CREATE_WITH_ITEM_TITLE);
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setTooltip(Blockly.Msg.LISTS_CREATE_WITH_ITEM_TOOLTIP);
+ this.contextMenu = false;
+ }
+};
+
+Blockly.Blocks['lists_repeat'] = {
+ /**
+ * Block for creating a list with one element repeated.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.LISTS_REPEAT_TITLE,
+ "args0": [
+ {
+ "type": "input_value",
+ "name": "ITEM"
+ },
+ {
+ "type": "input_value",
+ "name": "NUM",
+ "check": Blockly.Types.NUMBER.checkList
+ }
+ ],
+ "output": "Array",
+ "colour": Blockly.Blocks.lists.HUE,
+ "tooltip": Blockly.Msg.LISTS_REPEAT_TOOLTIP,
+ "helpUrl": Blockly.Msg.LISTS_REPEAT_HELPURL
+ });
+ }
+};
+
+Blockly.Blocks['lists_length'] = {
+ /**
+ * Block for list length.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.LISTS_LENGTH_TITLE,
+ "args0": [
+ {
+ "type": "input_value",
+ "name": "VALUE",
+ "check": Blockly.Types.TEXT.checkList.concat('Array')
+ }
+ ],
+ "output": Blockly.Types.NUMBER.output,
+ "colour": Blockly.Blocks.lists.HUE,
+ "tooltip": Blockly.Msg.LISTS_LENGTH_TOOLTIP,
+ "helpUrl": Blockly.Msg.LISTS_LENGTH_HELPURL
+ });
+ }
+};
+
+Blockly.Blocks['lists_isEmpty'] = {
+ /**
+ * Block for is the list empty?
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.LISTS_ISEMPTY_TITLE,
+ "args0": [
+ {
+ "type": "input_value",
+ "name": "VALUE",
+ "check": Blockly.Types.TEXT.checkList.concat('Array')
+ }
+ ],
+ "output": Blockly.Types.BOOLEAN.output,
+ "colour": Blockly.Blocks.lists.HUE,
+ "tooltip": Blockly.Msg.LISTS_ISEMPTY_TOOLTIP,
+ "helpUrl": Blockly.Msg.LISTS_ISEMPTY_HELPURL
+ });
+ }
+};
+
+Blockly.Blocks['lists_indexOf'] = {
+ /**
+ * Block for finding an item in the list.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var OPERATORS =
+ [[Blockly.Msg.LISTS_INDEX_OF_FIRST, 'FIRST'],
+ [Blockly.Msg.LISTS_INDEX_OF_LAST, 'LAST']];
+ this.setHelpUrl(Blockly.Msg.LISTS_INDEX_OF_HELPURL);
+ this.setColour(Blockly.Blocks.lists.HUE);
+ this.setOutput(true, Blockly.Types.NUMBER.output);
+ this.appendValueInput('VALUE')
+ .setCheck('Array')
+ .appendField(Blockly.Msg.LISTS_INDEX_OF_INPUT_IN_LIST);
+ this.appendValueInput('FIND')
+ .appendField(new Blockly.FieldDropdown(OPERATORS), 'END');
+ this.setInputsInline(true);
+ this.setTooltip(Blockly.Msg.LISTS_INDEX_OF_TOOLTIP);
+ }
+};
+
+Blockly.Blocks['lists_getIndex'] = {
+ /**
+ * Block for getting element at index.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var MODE =
+ [[Blockly.Msg.LISTS_GET_INDEX_GET, 'GET'],
+ [Blockly.Msg.LISTS_GET_INDEX_GET_REMOVE, 'GET_REMOVE'],
+ [Blockly.Msg.LISTS_GET_INDEX_REMOVE, 'REMOVE']];
+ this.WHERE_OPTIONS =
+ [[Blockly.Msg.LISTS_GET_INDEX_FROM_START, 'FROM_START'],
+ [Blockly.Msg.LISTS_GET_INDEX_FROM_END, 'FROM_END'],
+ [Blockly.Msg.LISTS_GET_INDEX_FIRST, 'FIRST'],
+ [Blockly.Msg.LISTS_GET_INDEX_LAST, 'LAST'],
+ [Blockly.Msg.LISTS_GET_INDEX_RANDOM, 'RANDOM']];
+ this.setHelpUrl(Blockly.Msg.LISTS_GET_INDEX_HELPURL);
+ this.setColour(Blockly.Blocks.lists.HUE);
+ var modeMenu = new Blockly.FieldDropdown(MODE, function(value) {
+ var isStatement = (value == 'REMOVE');
+ this.sourceBlock_.updateStatement_(isStatement);
+ });
+ this.appendValueInput('VALUE')
+ .setCheck('Array')
+ .appendField(Blockly.Msg.LISTS_GET_INDEX_INPUT_IN_LIST);
+ this.appendDummyInput()
+ .appendField(modeMenu, 'MODE')
+ .appendField('', 'SPACE');
+ this.appendDummyInput('AT');
+ if (Blockly.Msg.LISTS_GET_INDEX_TAIL) {
+ this.appendDummyInput('TAIL')
+ .appendField(Blockly.Msg.LISTS_GET_INDEX_TAIL);
+ }
+ this.setInputsInline(true);
+ this.setOutput(true);
+ this.updateAt_(true);
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ var combo = thisBlock.getFieldValue('MODE') + '_' +
+ thisBlock.getFieldValue('WHERE');
+ return Blockly.Msg['LISTS_GET_INDEX_TOOLTIP_' + combo];
+ });
+ },
+ /**
+ * Create XML to represent whether the block is a statement or a value.
+ * Also represent whether there is an 'AT' input.
+ * @return {Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ var isStatement = !this.outputConnection;
+ container.setAttribute('statement', isStatement);
+ var isAt = this.getInput('AT').type == Blockly.INPUT_VALUE;
+ container.setAttribute('at', isAt);
+ return container;
+ },
+ /**
+ * Parse XML to restore the 'AT' input.
+ * @param {!Element} xmlElement XML storage element.
+ * @this Blockly.Block
+ */
+ domToMutation: function(xmlElement) {
+ // Note: Until January 2013 this block did not have mutations,
+ // so 'statement' defaults to false and 'at' defaults to true.
+ var isStatement = (xmlElement.getAttribute('statement') == 'true');
+ this.updateStatement_(isStatement);
+ var isAt = (xmlElement.getAttribute('at') != 'false');
+ this.updateAt_(isAt);
+ },
+ /**
+ * Switch between a value block and a statement block.
+ * @param {boolean} newStatement True if the block should be a statement.
+ * False if the block should be a value.
+ * @private
+ * @this Blockly.Block
+ */
+ updateStatement_: function(newStatement) {
+ var oldStatement = !this.outputConnection;
+ if (newStatement != oldStatement) {
+ this.unplug(true, true);
+ if (newStatement) {
+ this.setOutput(false);
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ } else {
+ this.setPreviousStatement(false);
+ this.setNextStatement(false);
+ this.setOutput(true);
+ }
+ }
+ },
+ /**
+ * Create or delete an input for the numeric index.
+ * @param {boolean} isAt True if the input should exist.
+ * @private
+ * @this Blockly.Block
+ */
+ updateAt_: function(isAt) {
+ // Destroy old 'AT' and 'ORDINAL' inputs.
+ this.removeInput('AT');
+ this.removeInput('ORDINAL', true);
+ // Create either a value 'AT' input or a dummy input.
+ if (isAt) {
+ this.appendValueInput('AT').setCheck(Blockly.Types.NUMBER.checkList);
+ if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) {
+ this.appendDummyInput('ORDINAL')
+ .appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX);
+ }
+ } else {
+ this.appendDummyInput('AT');
+ }
+ var menu = new Blockly.FieldDropdown(this.WHERE_OPTIONS, function(value) {
+ var newAt = (value == 'FROM_START') || (value == 'FROM_END');
+ // The 'isAt' variable is available due to this function being a closure.
+ if (newAt != isAt) {
+ var block = this.sourceBlock_;
+ block.updateAt_(newAt);
+ // This menu has been destroyed and replaced. Update the replacement.
+ block.setFieldValue(value, 'WHERE');
+ return null;
+ }
+ return undefined;
+ });
+ this.getInput('AT').appendField(menu, 'WHERE');
+ if (Blockly.Msg.LISTS_GET_INDEX_TAIL) {
+ this.moveInputBefore('TAIL', null);
+ }
+ }
+};
+
+Blockly.Blocks['lists_setIndex'] = {
+ /**
+ * Block for setting the element at index.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var MODE =
+ [[Blockly.Msg.LISTS_SET_INDEX_SET, 'SET'],
+ [Blockly.Msg.LISTS_SET_INDEX_INSERT, 'INSERT']];
+ this.WHERE_OPTIONS =
+ [[Blockly.Msg.LISTS_GET_INDEX_FROM_START, 'FROM_START'],
+ [Blockly.Msg.LISTS_GET_INDEX_FROM_END, 'FROM_END'],
+ [Blockly.Msg.LISTS_GET_INDEX_FIRST, 'FIRST'],
+ [Blockly.Msg.LISTS_GET_INDEX_LAST, 'LAST'],
+ [Blockly.Msg.LISTS_GET_INDEX_RANDOM, 'RANDOM']];
+ this.setHelpUrl(Blockly.Msg.LISTS_SET_INDEX_HELPURL);
+ this.setColour(Blockly.Blocks.lists.HUE);
+ this.appendValueInput('LIST')
+ .setCheck('Array')
+ .appendField(Blockly.Msg.LISTS_SET_INDEX_INPUT_IN_LIST);
+ this.appendDummyInput()
+ .appendField(new Blockly.FieldDropdown(MODE), 'MODE')
+ .appendField('', 'SPACE');
+ this.appendDummyInput('AT');
+ this.appendValueInput('TO')
+ .appendField(Blockly.Msg.LISTS_SET_INDEX_INPUT_TO);
+ this.setInputsInline(true);
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setTooltip(Blockly.Msg.LISTS_SET_INDEX_TOOLTIP);
+ this.updateAt_(true);
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ var combo = thisBlock.getFieldValue('MODE') + '_' +
+ thisBlock.getFieldValue('WHERE');
+ return Blockly.Msg['LISTS_SET_INDEX_TOOLTIP_' + combo];
+ });
+ },
+ /**
+ * Create XML to represent whether there is an 'AT' input.
+ * @return {Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ var isAt = this.getInput('AT').type == Blockly.INPUT_VALUE;
+ container.setAttribute('at', isAt);
+ return container;
+ },
+ /**
+ * Parse XML to restore the 'AT' input.
+ * @param {!Element} xmlElement XML storage element.
+ * @this Blockly.Block
+ */
+ domToMutation: function(xmlElement) {
+ // Note: Until January 2013 this block did not have mutations,
+ // so 'at' defaults to true.
+ var isAt = (xmlElement.getAttribute('at') != 'false');
+ this.updateAt_(isAt);
+ },
+ /**
+ * Create or delete an input for the numeric index.
+ * @param {boolean} isAt True if the input should exist.
+ * @private
+ * @this Blockly.Block
+ */
+ updateAt_: function(isAt) {
+ // Destroy old 'AT' and 'ORDINAL' input.
+ this.removeInput('AT');
+ this.removeInput('ORDINAL', true);
+ // Create either a value 'AT' input or a dummy input.
+ if (isAt) {
+ this.appendValueInput('AT').setCheck(Blockly.Types.NUMBER.checkList);
+ if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) {
+ this.appendDummyInput('ORDINAL')
+ .appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX);
+ }
+ } else {
+ this.appendDummyInput('AT');
+ }
+ var menu = new Blockly.FieldDropdown(this.WHERE_OPTIONS, function(value) {
+ var newAt = (value == 'FROM_START') || (value == 'FROM_END');
+ // The 'isAt' variable is available due to this function being a closure.
+ if (newAt != isAt) {
+ var block = this.sourceBlock_;
+ block.updateAt_(newAt);
+ // This menu has been destroyed and replaced. Update the replacement.
+ block.setFieldValue(value, 'WHERE');
+ return null;
+ }
+ return undefined;
+ });
+ this.moveInputBefore('AT', 'TO');
+ if (this.getInput('ORDINAL')) {
+ this.moveInputBefore('ORDINAL', 'TO');
+ }
+
+ this.getInput('AT').appendField(menu, 'WHERE');
+ }
+};
+
+Blockly.Blocks['lists_getSublist'] = {
+ /**
+ * Block for getting sublist.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this['WHERE_OPTIONS_1'] =
+ [[Blockly.Msg.LISTS_GET_SUBLIST_START_FROM_START, 'FROM_START'],
+ [Blockly.Msg.LISTS_GET_SUBLIST_START_FROM_END, 'FROM_END'],
+ [Blockly.Msg.LISTS_GET_SUBLIST_START_FIRST, 'FIRST']];
+ this['WHERE_OPTIONS_2'] =
+ [[Blockly.Msg.LISTS_GET_SUBLIST_END_FROM_START, 'FROM_START'],
+ [Blockly.Msg.LISTS_GET_SUBLIST_END_FROM_END, 'FROM_END'],
+ [Blockly.Msg.LISTS_GET_SUBLIST_END_LAST, 'LAST']];
+ this.setHelpUrl(Blockly.Msg.LISTS_GET_SUBLIST_HELPURL);
+ this.setColour(Blockly.Blocks.lists.HUE);
+ this.appendValueInput('LIST')
+ .setCheck('Array')
+ .appendField(Blockly.Msg.LISTS_GET_SUBLIST_INPUT_IN_LIST);
+ this.appendDummyInput('AT1');
+ this.appendDummyInput('AT2');
+ if (Blockly.Msg.LISTS_GET_SUBLIST_TAIL) {
+ this.appendDummyInput('TAIL')
+ .appendField(Blockly.Msg.LISTS_GET_SUBLIST_TAIL);
+ }
+ this.setInputsInline(true);
+ this.setOutput(true, 'Array');
+ this.updateAt_(1, true);
+ this.updateAt_(2, true);
+ this.setTooltip(Blockly.Msg.LISTS_GET_SUBLIST_TOOLTIP);
+ },
+ /**
+ * Create XML to represent whether there are 'AT' inputs.
+ * @return {Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ var isAt1 = this.getInput('AT1').type == Blockly.INPUT_VALUE;
+ container.setAttribute('at1', isAt1);
+ var isAt2 = this.getInput('AT2').type == Blockly.INPUT_VALUE;
+ container.setAttribute('at2', isAt2);
+ return container;
+ },
+ /**
+ * Parse XML to restore the 'AT' inputs.
+ * @param {!Element} xmlElement XML storage element.
+ * @this Blockly.Block
+ */
+ domToMutation: function(xmlElement) {
+ var isAt1 = (xmlElement.getAttribute('at1') == 'true');
+ var isAt2 = (xmlElement.getAttribute('at2') == 'true');
+ this.updateAt_(1, isAt1);
+ this.updateAt_(2, isAt2);
+ },
+ /**
+ * Create or delete an input for a numeric index.
+ * This block has two such inputs, independant of each other.
+ * @param {number} n Specify first or second input (1 or 2).
+ * @param {boolean} isAt True if the input should exist.
+ * @private
+ * @this Blockly.Block
+ */
+ updateAt_: function(n, isAt) {
+ // Create or delete an input for the numeric index.
+ // Destroy old 'AT' and 'ORDINAL' inputs.
+ this.removeInput('AT' + n);
+ this.removeInput('ORDINAL' + n, true);
+ // Create either a value 'AT' input or a dummy input.
+ if (isAt) {
+ this.appendValueInput('AT' + n).setCheck(Blockly.Types.NUMBER.checkList);
+ if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) {
+ this.appendDummyInput('ORDINAL' + n)
+ .appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX);
+ }
+ } else {
+ this.appendDummyInput('AT' + n);
+ }
+ var menu = new Blockly.FieldDropdown(this['WHERE_OPTIONS_' + n],
+ function(value) {
+ var newAt = (value == 'FROM_START') || (value == 'FROM_END');
+ // The 'isAt' variable is available due to this function being a closure.
+ if (newAt != isAt) {
+ var block = this.sourceBlock_;
+ block.updateAt_(n, newAt);
+ // This menu has been destroyed and replaced. Update the replacement.
+ block.setFieldValue(value, 'WHERE' + n);
+ return null;
+ }
+ return undefined;
+ });
+ this.getInput('AT' + n)
+ .appendField(menu, 'WHERE' + n);
+ if (n == 1) {
+ this.moveInputBefore('AT1', 'AT2');
+ if (this.getInput('ORDINAL1')) {
+ this.moveInputBefore('ORDINAL1', 'AT2');
+ }
+ }
+ if (Blockly.Msg.LISTS_GET_SUBLIST_TAIL) {
+ this.moveInputBefore('TAIL', null);
+ }
+ }
+};
+
+Blockly.Blocks['lists_sort'] = {
+ /**
+ * Block for sorting a list.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.LISTS_SORT_TITLE,
+ "args0": [
+ {
+ "type": "field_dropdown",
+ "name": "TYPE",
+ "options": [
+ [Blockly.Msg.LISTS_SORT_TYPE_NUMERIC, "NUMERIC"],
+ [Blockly.Msg.LISTS_SORT_TYPE_TEXT, "TEXT"],
+ [Blockly.Msg.LISTS_SORT_TYPE_IGNORECASE, "IGNORE_CASE"]
+ ]
+ },
+ {
+ "type": "field_dropdown",
+ "name": "DIRECTION",
+ "options": [
+ [Blockly.Msg.LISTS_SORT_ORDER_ASCENDING, "1"],
+ [Blockly.Msg.LISTS_SORT_ORDER_DESCENDING, "-1"]
+ ]
+ },
+ {
+ "type": "input_value",
+ "name": "LIST",
+ "check": "Array"
+ }
+ ],
+ "output": "Array",
+ "colour": Blockly.Blocks.lists.HUE,
+ "tooltip": Blockly.Msg.LISTS_SORT_TOOLTIP,
+ "helpUrl": Blockly.Msg.LISTS_SORT_HELPURL
+ });
+ }
+};
+
+Blockly.Blocks['lists_split'] = {
+ /**
+ * Block for splitting text into a list, or joining a list into text.
+ * @this Blockly.Block
+ */
+ init: function() {
+ // Assign 'this' to a variable for use in the closures below.
+ var thisBlock = this;
+ var dropdown = new Blockly.FieldDropdown(
+ [[Blockly.Msg.LISTS_SPLIT_LIST_FROM_TEXT, 'SPLIT'],
+ [Blockly.Msg.LISTS_SPLIT_TEXT_FROM_LIST, 'JOIN']],
+ function(newMode) {
+ thisBlock.updateType_(newMode);
+ });
+ this.setHelpUrl(Blockly.Msg.LISTS_SPLIT_HELPURL);
+ this.setColour(Blockly.Blocks.lists.HUE);
+ this.appendValueInput('INPUT')
+ .setCheck(Blockly.Types.TEXT.checkList)
+ .appendField(dropdown, 'MODE');
+ this.appendValueInput('DELIM')
+ .setCheck(Blockly.Types.TEXT.checkList)
+ .appendField(Blockly.Msg.LISTS_SPLIT_WITH_DELIMITER);
+ this.setInputsInline(true);
+ this.setOutput(true, 'Array');
+ this.setTooltip(function() {
+ var mode = thisBlock.getFieldValue('MODE');
+ if (mode == 'SPLIT') {
+ return Blockly.Msg.LISTS_SPLIT_TOOLTIP_SPLIT;
+ } else if (mode == 'JOIN') {
+ return Blockly.Msg.LISTS_SPLIT_TOOLTIP_JOIN;
+ }
+ throw 'Unknown mode: ' + mode;
+ });
+ },
+ /**
+ * Modify this block to have the correct input and output types.
+ * @param {string} newMode Either 'SPLIT' or 'JOIN'.
+ * @private
+ * @this Blockly.Block
+ */
+ updateType_: function(newMode) {
+ if (newMode == 'SPLIT') {
+ this.outputConnection.setCheck('Array');
+ this.getInput('INPUT').setCheck(Blockly.Types.TEXT.checkList);
+ } else {
+ this.outputConnection.setCheck(Blockly.Types.TEXT.checkList);
+ this.getInput('INPUT').setCheck('Array');
+ }
+ },
+ /**
+ * Create XML to represent the input and output types.
+ * @return {!Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ container.setAttribute('mode', this.getFieldValue('MODE'));
+ return container;
+ },
+ /**
+ * Parse XML to restore the input and output types.
+ * @param {!Element} xmlElement XML storage element.
+ * @this Blockly.Block
+ */
+ domToMutation: function(xmlElement) {
+ this.updateType_(xmlElement.getAttribute('mode'));
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/logic.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/logic.js
new file mode 100644
index 000000000..32c9eccaf
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/logic.js
@@ -0,0 +1,508 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2012 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Logic blocks for Blockly.
+ * @author q.neutron@gmail.com (Quynh Neutron)
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.logic');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/**
+ * Common HSV hue for all blocks in this category.
+ */
+Blockly.Blocks.logic.HUE = 210;
+
+Blockly.Blocks['controls_if'] = {
+ /**
+ * Block for if/elseif/else condition.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl(Blockly.Msg.CONTROLS_IF_HELPURL);
+ this.setColour(Blockly.Blocks.logic.HUE);
+ this.appendValueInput('IF0')
+ .setCheck(Blockly.Types.BOOLEAN.checkList)
+ .appendField(Blockly.Msg.CONTROLS_IF_MSG_IF);
+ this.appendStatementInput('DO0')
+ .appendField(Blockly.Msg.CONTROLS_IF_MSG_THEN);
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setMutator(new Blockly.Mutator(['controls_if_elseif',
+ 'controls_if_else']));
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ if (!thisBlock.elseifCount_ && !thisBlock.elseCount_) {
+ return Blockly.Msg.CONTROLS_IF_TOOLTIP_1;
+ } else if (!thisBlock.elseifCount_ && thisBlock.elseCount_) {
+ return Blockly.Msg.CONTROLS_IF_TOOLTIP_2;
+ } else if (thisBlock.elseifCount_ && !thisBlock.elseCount_) {
+ return Blockly.Msg.CONTROLS_IF_TOOLTIP_3;
+ } else if (thisBlock.elseifCount_ && thisBlock.elseCount_) {
+ return Blockly.Msg.CONTROLS_IF_TOOLTIP_4;
+ }
+ return '';
+ });
+ this.elseifCount_ = 0;
+ this.elseCount_ = 0;
+ },
+ /**
+ * Create XML to represent the number of else-if and else inputs.
+ * @return {Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ if (!this.elseifCount_ && !this.elseCount_) {
+ return null;
+ }
+ var 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: function(xmlElement) {
+ this.elseifCount_ = parseInt(xmlElement.getAttribute('elseif'), 10) || 0;
+ this.elseCount_ = parseInt(xmlElement.getAttribute('else'), 10) || 0;
+ this.updateShape_();
+ },
+ /**
+ * Populate the mutator's dialog with this block's components.
+ * @param {!Blockly.Workspace} workspace Mutator's workspace.
+ * @return {!Blockly.Block} Root block in mutator.
+ * @this Blockly.Block
+ */
+ decompose: function(workspace) {
+ var containerBlock = workspace.newBlock('controls_if_if');
+ containerBlock.initSvg();
+ var connection = containerBlock.nextConnection;
+ for (var i = 1; i <= this.elseifCount_; i++) {
+ var elseifBlock = workspace.newBlock('controls_if_elseif');
+ elseifBlock.initSvg();
+ connection.connect(elseifBlock.previousConnection);
+ connection = elseifBlock.nextConnection;
+ }
+ if (this.elseCount_) {
+ var elseBlock = workspace.newBlock('controls_if_else');
+ elseBlock.initSvg();
+ connection.connect(elseBlock.previousConnection);
+ }
+ return containerBlock;
+ },
+ /**
+ * Reconfigure this block based on the mutator dialog's components.
+ * @param {!Blockly.Block} containerBlock Root block in mutator.
+ * @this Blockly.Block
+ */
+ compose: function(containerBlock) {
+ var clauseBlock = containerBlock.nextConnection.targetBlock();
+ // Count number of inputs.
+ this.elseifCount_ = 0;
+ this.elseCount_ = 0;
+ var valueConnections = [null];
+ var statementConnections = [null];
+ var elseStatementConnection = null;
+ while (clauseBlock) {
+ switch (clauseBlock.type) {
+ case 'controls_if_elseif':
+ this.elseifCount_++;
+ valueConnections.push(clauseBlock.valueConnection_);
+ statementConnections.push(clauseBlock.statementConnection_);
+ break;
+ case 'controls_if_else':
+ this.elseCount_++;
+ elseStatementConnection = clauseBlock.statementConnection_;
+ break;
+ default:
+ throw 'Unknown block type.';
+ }
+ clauseBlock = clauseBlock.nextConnection &&
+ clauseBlock.nextConnection.targetBlock();
+ }
+ this.updateShape_();
+ // Reconnect any child blocks.
+ for (var i = 1; i <= this.elseifCount_; i++) {
+ Blockly.Mutator.reconnect(valueConnections[i], this, 'IF' + i);
+ Blockly.Mutator.reconnect(statementConnections[i], this, 'DO' + i);
+ }
+ Blockly.Mutator.reconnect(elseStatementConnection, this, 'ELSE');
+ },
+ /**
+ * Store pointers to any connected child blocks.
+ * @param {!Blockly.Block} containerBlock Root block in mutator.
+ * @this Blockly.Block
+ */
+ saveConnections: function(containerBlock) {
+ var clauseBlock = containerBlock.nextConnection.targetBlock();
+ var i = 1;
+ while (clauseBlock) {
+ switch (clauseBlock.type) {
+ case 'controls_if_elseif':
+ var inputIf = this.getInput('IF' + i);
+ var inputDo = this.getInput('DO' + i);
+ clauseBlock.valueConnection_ =
+ inputIf && inputIf.connection.targetConnection;
+ clauseBlock.statementConnection_ =
+ inputDo && inputDo.connection.targetConnection;
+ i++;
+ break;
+ case 'controls_if_else':
+ var inputDo = this.getInput('ELSE');
+ clauseBlock.statementConnection_ =
+ inputDo && inputDo.connection.targetConnection;
+ break;
+ default:
+ throw 'Unknown block type.';
+ }
+ clauseBlock = clauseBlock.nextConnection &&
+ clauseBlock.nextConnection.targetBlock();
+ }
+ },
+ /**
+ * Modify this block to have the correct number of inputs.
+ * @private
+ * @this Blockly.Block
+ */
+ updateShape_: function() {
+ // Delete everything.
+ if (this.getInput('ELSE')) {
+ this.removeInput('ELSE');
+ }
+ var i = 1;
+ while (this.getInput('IF' + i)) {
+ this.removeInput('IF' + i);
+ this.removeInput('DO' + i);
+ i++;
+ }
+ // Rebuild block.
+ for (var i = 1; i <= this.elseifCount_; i++) {
+ this.appendValueInput('IF' + i)
+ .setCheck('Boolean')
+ .appendField(Blockly.Msg.CONTROLS_IF_MSG_ELSEIF);
+ this.appendStatementInput('DO' + i)
+ .appendField(Blockly.Msg.CONTROLS_IF_MSG_THEN);
+ }
+ if (this.elseCount_) {
+ this.appendStatementInput('ELSE')
+ .appendField(Blockly.Msg.CONTROLS_IF_MSG_ELSE);
+ }
+ }
+};
+
+Blockly.Blocks['controls_if_if'] = {
+ /**
+ * Mutator block for if container.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setColour(Blockly.Blocks.logic.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.CONTROLS_IF_IF_TITLE_IF);
+ this.setNextStatement(true);
+ this.setTooltip(Blockly.Msg.CONTROLS_IF_IF_TOOLTIP);
+ this.contextMenu = false;
+ }
+};
+
+Blockly.Blocks['controls_if_elseif'] = {
+ /**
+ * Mutator bolck for else-if condition.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setColour(Blockly.Blocks.logic.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.CONTROLS_IF_ELSEIF_TITLE_ELSEIF);
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setTooltip(Blockly.Msg.CONTROLS_IF_ELSEIF_TOOLTIP);
+ this.contextMenu = false;
+ }
+};
+
+Blockly.Blocks['controls_if_else'] = {
+ /**
+ * Mutator block for else condition.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setColour(Blockly.Blocks.logic.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.CONTROLS_IF_ELSE_TITLE_ELSE);
+ this.setPreviousStatement(true);
+ this.setTooltip(Blockly.Msg.CONTROLS_IF_ELSE_TOOLTIP);
+ this.contextMenu = false;
+ }
+};
+
+Blockly.Blocks['logic_compare'] = {
+ /**
+ * Block for comparison operator.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var OPERATORS = this.RTL ? [
+ ['=', 'EQ'],
+ ['\u2260', 'NEQ'],
+ ['>', 'LT'],
+ ['\u2265', 'LTE'],
+ ['<', 'GT'],
+ ['\u2264', 'GTE']
+ ] : [
+ ['=', 'EQ'],
+ ['\u2260', 'NEQ'],
+ ['<', 'LT'],
+ ['\u2264', 'LTE'],
+ ['>', 'GT'],
+ ['\u2265', 'GTE']
+ ];
+ this.setHelpUrl(Blockly.Msg.LOGIC_COMPARE_HELPURL);
+ this.setColour(Blockly.Blocks.logic.HUE);
+ this.setOutput(true, Blockly.Types.BOOLEAN.output);
+ this.appendValueInput('A');
+ this.appendValueInput('B')
+ .appendField(new Blockly.FieldDropdown(OPERATORS), 'OP');
+ this.setInputsInline(true);
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ var op = thisBlock.getFieldValue('OP');
+ var TOOLTIPS = {
+ 'EQ': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_EQ,
+ 'NEQ': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_NEQ,
+ 'LT': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_LT,
+ 'LTE': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_LTE,
+ 'GT': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_GT,
+ 'GTE': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_GTE
+ };
+ return TOOLTIPS[op];
+ });
+ this.prevBlocks_ = [null, null];
+ },
+ /**
+ * Called whenever anything on the workspace changes.
+ * Prevent mismatched types from being compared.
+ * @param {!Blockly.Events.Abstract} e Change event.
+ * @this Blockly.Block
+ */
+ onchange: function(e) {
+ var blockA = this.getInputTargetBlock('A');
+ var blockB = this.getInputTargetBlock('B');
+ // Disconnect blocks that existed prior to this change if they don't match.
+ if (blockA && blockB &&
+ !blockA.outputConnection.checkType_(blockB.outputConnection)) {
+ // Mismatch between two inputs. Disconnect previous and bump it away.
+ // Ensure that any disconnections are grouped with the causing event.
+ Blockly.Events.setGroup(e.group);
+ for (var i = 0; i < this.prevBlocks_.length; i++) {
+ var block = this.prevBlocks_[i];
+ if (block === blockA || block === blockB) {
+ block.unplug();
+ block.bumpNeighbours_();
+ }
+ }
+ Blockly.Events.setGroup(false);
+ }
+ this.prevBlocks_[0] = blockA;
+ this.prevBlocks_[1] = blockB;
+ },
+ /** Assigns a type to the block, comparison operations result in booleans. */
+ getBlockType: function() {
+ return Blockly.Types.BOOLEAN;
+ }
+};
+
+Blockly.Blocks['logic_operation'] = {
+ /**
+ * Block for logical operations: 'and', 'or'.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var OPERATORS =
+ [[Blockly.Msg.LOGIC_OPERATION_AND, 'AND'],
+ [Blockly.Msg.LOGIC_OPERATION_OR, 'OR']];
+ this.setHelpUrl(Blockly.Msg.LOGIC_OPERATION_HELPURL);
+ this.setColour(Blockly.Blocks.logic.HUE);
+ this.setOutput(true, Blockly.Types.BOOLEAN.output);
+ this.appendValueInput('A')
+ .setCheck(Blockly.Types.BOOLEAN.checkList);
+ this.appendValueInput('B')
+ .setCheck(Blockly.Types.BOOLEAN.checkList)
+ .appendField(new Blockly.FieldDropdown(OPERATORS), 'OP');
+ this.setInputsInline(true);
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ var op = thisBlock.getFieldValue('OP');
+ var TOOLTIPS = {
+ 'AND': Blockly.Msg.LOGIC_OPERATION_TOOLTIP_AND,
+ 'OR': Blockly.Msg.LOGIC_OPERATION_TOOLTIP_OR
+ };
+ return TOOLTIPS[op];
+ });
+ },
+ /** Assigns a block type, logic comparison operations result in bools. */
+ getBlockType: function() {
+ return Blockly.Types.BOOLEAN;
+ }
+};
+
+Blockly.Blocks['logic_negate'] = {
+ /**
+ * Block for negation.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.LOGIC_NEGATE_TITLE,
+ "args0": [
+ {
+ "type": "input_value",
+ "name": "BOOL",
+ "check": Blockly.Types.BOOLEAN.checkList
+ }
+ ],
+ "output": Blockly.Types.BOOLEAN.output,
+ "colour": Blockly.Blocks.logic.HUE,
+ "tooltip": Blockly.Msg.LOGIC_NEGATE_TOOLTIP,
+ "helpUrl": Blockly.Msg.LOGIC_NEGATE_HELPURL
+ });
+ },
+ /** Assigns block type, 'block input' is meant to be a boolean, so same. */
+ getBlockType: function() {
+ return Blockly.Types.BOOLEAN;
+ }
+};
+
+Blockly.Blocks['logic_boolean'] = {
+ /**
+ * Block for boolean data type: true and false.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": "%1",
+ "args0": [
+ {
+ "type": "field_dropdown",
+ "name": "BOOL",
+ "options": [
+ [Blockly.Msg.LOGIC_BOOLEAN_TRUE, "TRUE"],
+ [Blockly.Msg.LOGIC_BOOLEAN_FALSE, "FALSE"]
+ ]
+ }
+ ],
+ "output": Blockly.Types.BOOLEAN.output,
+ "colour": Blockly.Blocks.logic.HUE,
+ "tooltip": Blockly.Msg.LOGIC_BOOLEAN_TOOLTIP,
+ "helpUrl": Blockly.Msg.LOGIC_BOOLEAN_HELPURL
+ });
+ },
+ /** Assigns a type to the boolean block. */
+ getBlockType: function() {
+ return Blockly.Types.BOOLEAN;
+ }
+};
+
+Blockly.Blocks['logic_null'] = {
+ /**
+ * Block for null data type.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.LOGIC_NULL,
+ "output": Blockly.Types.NULL.output,
+ "colour": Blockly.Blocks.logic.HUE,
+ "tooltip": Blockly.Msg.LOGIC_NULL_TOOLTIP,
+ "helpUrl": Blockly.Msg.LOGIC_NULL_HELPURL
+ });
+ },
+ /** Assigns a type to the NULL block. */
+ getBlockType: function() {
+ return Blockly.Types.NULL;
+ }
+};
+
+Blockly.Blocks['logic_ternary'] = {
+ /**
+ * Block for ternary operator.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl(Blockly.Msg.LOGIC_TERNARY_HELPURL);
+ this.setColour(Blockly.Blocks.logic.HUE);
+ this.appendValueInput('IF')
+ .setCheck(Blockly.Types.BOOLEAN.checkList)
+ .appendField(Blockly.Msg.LOGIC_TERNARY_CONDITION);
+ this.appendValueInput('THEN')
+ .appendField(Blockly.Msg.LOGIC_TERNARY_IF_TRUE);
+ this.appendValueInput('ELSE')
+ .appendField(Blockly.Msg.LOGIC_TERNARY_IF_FALSE);
+ this.setOutput(true);
+ this.setTooltip(Blockly.Msg.LOGIC_TERNARY_TOOLTIP);
+ this.prevParentConnection_ = null;
+ },
+ /**
+ * Called whenever anything on the workspace changes.
+ * Prevent mismatched types.
+ * @param {!Blockly.Events.Abstract} e Change event.
+ * @this Blockly.Block
+ */
+ onchange: function(e) {
+ var blockA = this.getInputTargetBlock('THEN');
+ var blockB = this.getInputTargetBlock('ELSE');
+ var parentConnection = this.outputConnection.targetConnection;
+ // Disconnect blocks that existed prior to this change if they don't match.
+ if ((blockA || blockB) && parentConnection) {
+ for (var i = 0; i < 2; i++) {
+ var block = (i == 1) ? blockA : blockB;
+ if (block && !block.outputConnection.checkType_(parentConnection)) {
+ // Ensure that any disconnections are grouped with the causing event.
+ Blockly.Events.setGroup(e.group);
+ if (parentConnection === this.prevParentConnection_) {
+ this.unplug();
+ parentConnection.getSourceBlock().bumpNeighbours_();
+ } else {
+ block.unplug();
+ block.bumpNeighbours_();
+ }
+ Blockly.Events.setGroup(false);
+ }
+ }
+ }
+ this.prevParentConnection_ = parentConnection;
+ }
+ //TODO: getBlockType that uses the type of the given inputs
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/loops.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/loops.js
new file mode 100644
index 000000000..928562c64
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/loops.js
@@ -0,0 +1,297 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2012 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Loop blocks for Blockly.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.loops');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/**
+ * Common HSV hue for all blocks in this category.
+ */
+Blockly.Blocks.loops.HUE = 120;
+
+Blockly.Blocks['controls_repeat_ext'] = {
+ /**
+ * Block for repeat n times (external number).
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.CONTROLS_REPEAT_TITLE,
+ "args0": [
+ {
+ "type": "input_value",
+ "name": "TIMES",
+ "check": Blockly.Types.NUMBER.checkList
+ }
+ ],
+ "previousStatement": null,
+ "nextStatement": null,
+ "colour": Blockly.Blocks.loops.HUE,
+ "tooltip": Blockly.Msg.CONTROLS_REPEAT_TOOLTIP,
+ "helpUrl": Blockly.Msg.CONTROLS_REPEAT_HELPURL
+ });
+ this.appendStatementInput('DO')
+ .appendField(Blockly.Msg.CONTROLS_REPEAT_INPUT_DO);
+ }
+};
+
+Blockly.Blocks['controls_repeat'] = {
+ /**
+ * Block for repeat n times (internal number).
+ * The 'controls_repeat_ext' block is preferred as it is more flexible.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.CONTROLS_REPEAT_TITLE,
+ "args0": [
+ {
+ "type": "field_number",
+ "name": "TIMES",
+ "check": Blockly.Types.NUMBER.checkList,
+ "text": "10"
+ }
+ ],
+ "previousStatement": null,
+ "nextStatement": null,
+ "colour": Blockly.Blocks.loops.HUE,
+ "tooltip": Blockly.Msg.CONTROLS_REPEAT_TOOLTIP,
+ "helpUrl": Blockly.Msg.CONTROLS_REPEAT_HELPURL
+ });
+ this.appendStatementInput('DO')
+ .appendField(Blockly.Msg.CONTROLS_REPEAT_INPUT_DO);
+ this.getField('TIMES').setValidator(
+ Blockly.FieldTextInput.nonnegativeIntegerValidator);
+ }
+};
+
+Blockly.Blocks['controls_whileUntil'] = {
+ /**
+ * Block for 'do while/until' loop.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var OPERATORS =
+ [[Blockly.Msg.CONTROLS_WHILEUNTIL_OPERATOR_WHILE, 'WHILE'],
+ [Blockly.Msg.CONTROLS_WHILEUNTIL_OPERATOR_UNTIL, 'UNTIL']];
+ this.setHelpUrl(Blockly.Msg.CONTROLS_WHILEUNTIL_HELPURL);
+ this.setColour(Blockly.Blocks.loops.HUE);
+ this.appendValueInput('BOOL')
+ .setCheck(Blockly.Types.BOOLEAN.checkList)
+ .appendField(new Blockly.FieldDropdown(OPERATORS), 'MODE');
+ this.appendStatementInput('DO')
+ .appendField(Blockly.Msg.CONTROLS_WHILEUNTIL_INPUT_DO);
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ var op = thisBlock.getFieldValue('MODE');
+ var TOOLTIPS = {
+ 'WHILE': Blockly.Msg.CONTROLS_WHILEUNTIL_TOOLTIP_WHILE,
+ 'UNTIL': Blockly.Msg.CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL
+ };
+ return TOOLTIPS[op];
+ });
+ }
+};
+
+Blockly.Blocks['controls_for'] = {
+ /**
+ * Block for 'for' loop.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.CONTROLS_FOR_TITLE,
+ "args0": [
+ {
+ "type": "field_variable",
+ "name": "VAR",
+ "variable": null
+ },
+ {
+ "type": "input_value",
+ "name": "FROM",
+ "check": Blockly.Types.NUMBER.checkList,
+ "align": "RIGHT"
+ },
+ {
+ "type": "input_value",
+ "name": "TO",
+ "check": Blockly.Types.NUMBER.checkList,
+ "align": "RIGHT"
+ },
+ {
+ "type": "input_value",
+ "name": "BY",
+ "check": Blockly.Types.NUMBER.checkList,
+ "align": "RIGHT"
+ }
+ ],
+ "inputsInline": true,
+ "previousStatement": null,
+ "nextStatement": null,
+ "colour": Blockly.Blocks.loops.HUE,
+ "helpUrl": Blockly.Msg.CONTROLS_FOR_HELPURL
+ });
+ this.appendStatementInput('DO')
+ .appendField(Blockly.Msg.CONTROLS_FOR_INPUT_DO);
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ return Blockly.Msg.CONTROLS_FOR_TOOLTIP.replace('%1',
+ thisBlock.getFieldValue('VAR'));
+ });
+ },
+ /**
+ * Add menu option to create getter block for loop variable.
+ * @param {!Array} options List of menu options to add to.
+ * @this Blockly.Block
+ */
+ customContextMenu: function(options) {
+ if (!this.isCollapsed()) {
+ var option = {enabled: true};
+ var name = this.getFieldValue('VAR');
+ option.text = Blockly.Msg.VARIABLES_SET_CREATE_GET.replace('%1', name);
+ var xmlField = goog.dom.createDom('field', null, name);
+ xmlField.setAttribute('name', 'VAR');
+ var xmlBlock = goog.dom.createDom('block', null, xmlField);
+ xmlBlock.setAttribute('type', 'variables_get');
+ option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock);
+ options.push(option);
+ }
+ },
+ /**
+ * Defines the type of the variable selected in the drop down, an integer.
+ * @return {string} String to indicate the type if it has not been defined
+ * before.
+ */
+ getVarType: function(varName) {
+ return Blockly.Types.NUMBER;
+ }
+};
+
+Blockly.Blocks['controls_forEach'] = {
+ /**
+ * Block for 'for each' loop.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.CONTROLS_FOREACH_TITLE,
+ "args0": [
+ {
+ "type": "field_variable",
+ "name": "VAR",
+ "variable": null
+ },
+ {
+ "type": "input_value",
+ "name": "LIST",
+ "check": Blockly.Types.ARRAY.checkList
+ }
+ ],
+ "previousStatement": null,
+ "nextStatement": null,
+ "colour": Blockly.Blocks.loops.HUE,
+ "helpUrl": Blockly.Msg.CONTROLS_FOREACH_HELPURL
+ });
+ this.appendStatementInput('DO')
+ .appendField(Blockly.Msg.CONTROLS_FOREACH_INPUT_DO);
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ return Blockly.Msg.CONTROLS_FOREACH_TOOLTIP.replace('%1',
+ thisBlock.getFieldValue('VAR'));
+ });
+ },
+ customContextMenu: Blockly.Blocks['controls_for'].customContextMenu,
+ /** @returns {!string} The type of the variable used in this block */
+ getVarType: function(varName) {
+ return Blockly.Types.NUMBER;
+ }
+};
+
+Blockly.Blocks['controls_flow_statements'] = {
+ /**
+ * Block for flow statements: continue, break.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var OPERATORS =
+ [[Blockly.Msg.CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK, 'BREAK'],
+ [Blockly.Msg.CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE, 'CONTINUE']];
+ this.setHelpUrl(Blockly.Msg.CONTROLS_FLOW_STATEMENTS_HELPURL);
+ this.setColour(Blockly.Blocks.loops.HUE);
+ this.appendDummyInput()
+ .appendField(new Blockly.FieldDropdown(OPERATORS), 'FLOW');
+ this.setPreviousStatement(true);
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ var op = thisBlock.getFieldValue('FLOW');
+ var TOOLTIPS = {
+ 'BREAK': Blockly.Msg.CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK,
+ 'CONTINUE': Blockly.Msg.CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE
+ };
+ return TOOLTIPS[op];
+ });
+ },
+ /**
+ * 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: function(e) {
+ var legal = false;
+ // Is the block nested in a loop?
+ var block = this;
+ do {
+ if (this.LOOP_TYPES.indexOf(block.type) != -1) {
+ legal = true;
+ break;
+ }
+ block = block.getSurroundParent();
+ } while (block);
+ if (legal) {
+ this.setWarningText(null);
+ } else {
+ this.setWarningText(Blockly.Msg.CONTROLS_FLOW_STATEMENTS_WARNING);
+ }
+ },
+ /**
+ * List of block types that are loops and thus do not need warnings.
+ * To add a new loop type add this to your code:
+ * Blockly.Blocks['controls_flow_statements'].LOOP_TYPES.push('custom_loop');
+ */
+ LOOP_TYPES: ['controls_repeat', 'controls_repeat_ext', 'controls_forEach',
+ 'controls_for', 'controls_whileUntil']
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/math.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/math.js
new file mode 100644
index 000000000..9df246612
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/math.js
@@ -0,0 +1,608 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2012 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Math blocks for Blockly.
+ * @author q.neutron@gmail.com (Quynh Neutron)
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.math');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/**
+ * Common HSV hue for all blocks in this category.
+ */
+Blockly.Blocks.math.HUE = 230;
+
+Blockly.Blocks['math_number'] = {
+ /**
+ * Block for numeric value.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl(Blockly.Msg.MATH_NUMBER_HELPURL);
+ this.setColour(Blockly.Blocks.math.HUE);
+ this.appendDummyInput()
+ .appendField(
+ new Blockly.FieldTextInput(
+ '0', Blockly.FieldTextInput.numberValidator),
+ 'NUM');
+ this.setOutput(true, Blockly.Types.NUMBER.output);
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ // Number block is trivial. Use tooltip of parent block if it exists.
+ this.setTooltip(function() {
+ var parent = thisBlock.getParent();
+ return (parent && parent.getInputsInline() && parent.tooltip) ||
+ Blockly.Msg.MATH_NUMBER_TOOLTIP;
+ });
+ },
+ /**
+ * Reads the numerical value from the block and assigns a block type.
+ * @this Blockly.Block
+ */
+ getBlockType: function() {
+ var numString = this.getFieldValue('NUM');
+ return Blockly.Types.identifyNumber(numString);
+ }
+};
+
+Blockly.Blocks['math_arithmetic'] = {
+ /**
+ * Block for basic arithmetic operator.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": "%1 %2 %3",
+ "args0": [
+ {
+ "type": "input_value",
+ "name": "A",
+ "check": Blockly.Types.NUMBER.checkList
+ },
+ {
+ "type": "field_dropdown",
+ "name": "OP",
+ "options":
+ [[Blockly.Msg.MATH_ADDITION_SYMBOL, 'ADD'],
+ [Blockly.Msg.MATH_SUBTRACTION_SYMBOL, 'MINUS'],
+ [Blockly.Msg.MATH_MULTIPLICATION_SYMBOL, 'MULTIPLY'],
+ [Blockly.Msg.MATH_DIVISION_SYMBOL, 'DIVIDE'],
+ [Blockly.Msg.MATH_POWER_SYMBOL, 'POWER']]
+ },
+ {
+ "type": "input_value",
+ "name": "B",
+ "check": Blockly.Types.NUMBER.checkList
+ }
+ ],
+ "inputsInline": true,
+ "output": Blockly.Types.NUMBER.output,
+ "colour": Blockly.Blocks.math.HUE,
+ "helpUrl": Blockly.Msg.MATH_ARITHMETIC_HELPURL
+ });
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ var mode = thisBlock.getFieldValue('OP');
+ var TOOLTIPS = {
+ 'ADD': Blockly.Msg.MATH_ARITHMETIC_TOOLTIP_ADD,
+ 'MINUS': Blockly.Msg.MATH_ARITHMETIC_TOOLTIP_MINUS,
+ 'MULTIPLY': Blockly.Msg.MATH_ARITHMETIC_TOOLTIP_MULTIPLY,
+ 'DIVIDE': Blockly.Msg.MATH_ARITHMETIC_TOOLTIP_DIVIDE,
+ 'POWER': Blockly.Msg.MATH_ARITHMETIC_TOOLTIP_POWER
+ };
+ return TOOLTIPS[mode];
+ });
+ }
+ //TODO: a getBlockType based on the two input types following C++ rules
+};
+
+Blockly.Blocks['math_single'] = {
+ /**
+ * Block for advanced math operators with single operand.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": "%1 %2",
+ "args0": [
+ {
+ "type": "field_dropdown",
+ "name": "OP",
+ "options": [
+ [Blockly.Msg.MATH_SINGLE_OP_ROOT, 'ROOT'],
+ [Blockly.Msg.MATH_SINGLE_OP_ABSOLUTE, 'ABS'],
+ ['-', 'NEG'],
+ ['ln', 'LN'],
+ ['log10', 'LOG10'],
+ ['e^', 'EXP'],
+ ['10^', 'POW10']
+ ]
+ },
+ {
+ "type": "input_value",
+ "name": "NUM",
+ "check": Blockly.Types.DECIMAL.checkList
+ }
+ ],
+ "output": Blockly.Types.DECIMAL.output,
+ "colour": Blockly.Blocks.math.HUE,
+ "helpUrl": Blockly.Msg.MATH_SINGLE_HELPURL
+ });
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ var mode = thisBlock.getFieldValue('OP');
+ var TOOLTIPS = {
+ 'ROOT': Blockly.Msg.MATH_SINGLE_TOOLTIP_ROOT,
+ 'ABS': Blockly.Msg.MATH_SINGLE_TOOLTIP_ABS,
+ 'NEG': Blockly.Msg.MATH_SINGLE_TOOLTIP_NEG,
+ 'LN': Blockly.Msg.MATH_SINGLE_TOOLTIP_LN,
+ 'LOG10': Blockly.Msg.MATH_SINGLE_TOOLTIP_LOG10,
+ 'EXP': Blockly.Msg.MATH_SINGLE_TOOLTIP_EXP,
+ 'POW10': Blockly.Msg.MATH_SINGLE_TOOLTIP_POW10
+ };
+ return TOOLTIPS[mode];
+ });
+ },
+ /** @return {!string} Type of the block, all these operations are floats. */
+ getBlockType: function() {
+ return Blockly.Types.DECIMAL;
+ }
+};
+
+Blockly.Blocks['math_trig'] = {
+ /**
+ * Block for trigonometry operators.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": "%1 %2",
+ "args0": [
+ {
+ "type": "field_dropdown",
+ "name": "OP",
+ "options": [
+ [Blockly.Msg.MATH_TRIG_SIN, 'SIN'],
+ [Blockly.Msg.MATH_TRIG_COS, 'COS'],
+ [Blockly.Msg.MATH_TRIG_TAN, 'TAN'],
+ [Blockly.Msg.MATH_TRIG_ASIN, 'ASIN'],
+ [Blockly.Msg.MATH_TRIG_ACOS, 'ACOS'],
+ [Blockly.Msg.MATH_TRIG_ATAN, 'ATAN']
+ ]
+ },
+ {
+ "type": "input_value",
+ "name": "NUM",
+ "check": Blockly.Types.DECIMAL.checkList
+ }
+ ],
+ "output": Blockly.Types.DECIMAL.output,
+ "colour": Blockly.Blocks.math.HUE,
+ "helpUrl": Blockly.Msg.MATH_TRIG_HELPURL
+ });
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ var mode = thisBlock.getFieldValue('OP');
+ var TOOLTIPS = {
+ 'SIN': Blockly.Msg.MATH_TRIG_TOOLTIP_SIN,
+ 'COS': Blockly.Msg.MATH_TRIG_TOOLTIP_COS,
+ 'TAN': Blockly.Msg.MATH_TRIG_TOOLTIP_TAN,
+ 'ASIN': Blockly.Msg.MATH_TRIG_TOOLTIP_ASIN,
+ 'ACOS': Blockly.Msg.MATH_TRIG_TOOLTIP_ACOS,
+ 'ATAN': Blockly.Msg.MATH_TRIG_TOOLTIP_ATAN
+ };
+ return TOOLTIPS[mode];
+ });
+ },
+ /** @return {!string} Type of the block, all these operations are floats. */
+ getBlockType: function() {
+ return Blockly.Types.DECIMAL;
+ }
+};
+
+Blockly.Blocks['math_constant'] = {
+ /**
+ * Block for constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": "%1",
+ "args0": [
+ {
+ "type": "field_dropdown",
+ "name": "CONSTANT",
+ "options": [
+ ['\u03c0', 'PI'],
+ ['e', 'E'],
+ ['\u03c6', 'GOLDEN_RATIO'],
+ ['sqrt(2)', 'SQRT2'],
+ ['sqrt(\u00bd)', 'SQRT1_2'],
+ ['\u221e', 'INFINITY']
+ ]
+ }
+ ],
+ "output": Blockly.Types.DECIMAL.output,
+ "colour": Blockly.Blocks.math.HUE,
+ "tooltip": Blockly.Msg.MATH_CONSTANT_TOOLTIP,
+ "helpUrl": Blockly.Msg.MATH_CONSTANT_HELPURL
+ });
+ }
+};
+
+Blockly.Blocks['math_number_property'] = {
+ /**
+ * Block for checking if a number is even, odd, prime, whole, positive,
+ * negative or if it is divisible by certain number.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var PROPERTIES =
+ [[Blockly.Msg.MATH_IS_EVEN, 'EVEN'],
+ [Blockly.Msg.MATH_IS_ODD, 'ODD'],
+ [Blockly.Msg.MATH_IS_PRIME, 'PRIME'],
+ [Blockly.Msg.MATH_IS_WHOLE, 'WHOLE'],
+ [Blockly.Msg.MATH_IS_POSITIVE, 'POSITIVE'],
+ [Blockly.Msg.MATH_IS_NEGATIVE, 'NEGATIVE'],
+ [Blockly.Msg.MATH_IS_DIVISIBLE_BY, 'DIVISIBLE_BY']];
+ this.setColour(Blockly.Blocks.math.HUE);
+ this.appendValueInput('NUMBER_TO_CHECK')
+ .setCheck(Blockly.Types.NUMBER.checkList);
+ var dropdown = new Blockly.FieldDropdown(PROPERTIES, function(option) {
+ var divisorInput = (option == 'DIVISIBLE_BY');
+ this.sourceBlock_.updateShape_(divisorInput);
+ });
+ this.appendDummyInput()
+ .appendField(dropdown, 'PROPERTY');
+ this.setInputsInline(true);
+ this.setOutput(true, Blockly.Types.BOOLEAN.output);
+ this.setTooltip(Blockly.Msg.MATH_IS_TOOLTIP);
+ },
+ /**
+ * Create XML to represent whether the 'divisorInput' should be present.
+ * @return {Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ var divisorInput = (this.getFieldValue('PROPERTY') == 'DIVISIBLE_BY');
+ container.setAttribute('divisor_input', divisorInput);
+ return container;
+ },
+ /**
+ * Parse XML to restore the 'divisorInput'.
+ * @param {!Element} xmlElement XML storage element.
+ * @this Blockly.Block
+ */
+ domToMutation: function(xmlElement) {
+ var divisorInput = (xmlElement.getAttribute('divisor_input') == 'true');
+ this.updateShape_(divisorInput);
+ },
+ /**
+ * Modify this block to have (or not have) an input for 'is divisible by'.
+ * @param {boolean} divisorInput True if this block has a divisor input.
+ * @private
+ * @this Blockly.Block
+ */
+ updateShape_: function(divisorInput) {
+ // Add or remove a Value Input.
+ var inputExists = this.getInput('DIVISOR');
+ if (divisorInput) {
+ if (!inputExists) {
+ this.appendValueInput('DIVISOR')
+ .setCheck(Blockly.Types.NUMBER.checkList);
+ }
+ } else if (inputExists) {
+ this.removeInput('DIVISOR');
+ }
+ },
+ /** @return {!string} Type of the block, all these operations are bools. */
+ getBlockType: function() {
+ return Blockly.Types.BOOLEAN;
+ }
+};
+
+Blockly.Blocks['math_change'] = {
+ /**
+ * Block for adding to a variable in place.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.MATH_CHANGE_TITLE,
+ "args0": [
+ {
+ "type": "field_variable",
+ "name": "VAR",
+ "variable": Blockly.Msg.MATH_CHANGE_TITLE_ITEM
+ },
+ {
+ "type": "input_value",
+ "name": "DELTA",
+ "check": Blockly.Types.NUMBER.checkList,
+ "align": "RIGHT"
+ }
+ ],
+ "previousStatement": null,
+ "nextStatement": null,
+ "colour": Blockly.Blocks.math.HUE,
+ "helpUrl": Blockly.Msg.MATH_CHANGE_HELPURL
+ });
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ return Blockly.Msg.MATH_CHANGE_TOOLTIP.replace('%1',
+ thisBlock.getFieldValue('VAR'));
+ });
+ },
+ /**
+ * Gets the variable type selected in the drop down, always an integer.
+ * @param {!string} varName Name of the variable selected in this block to
+ * check.
+ * @return {string} String to indicate the variable type.
+ */
+ getVarType: function(varName) {
+ return Blockly.Types.NUMBER;
+ }
+};
+
+Blockly.Blocks['math_round'] = {
+ /**
+ * Block for rounding functions.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": "%1 %2",
+ "args0": [
+ {
+ "type": "field_dropdown",
+ "name": "OP",
+ "options": [
+ [Blockly.Msg.MATH_ROUND_OPERATOR_ROUND, 'ROUND'],
+ [Blockly.Msg.MATH_ROUND_OPERATOR_ROUNDUP, 'ROUNDUP'],
+ [Blockly.Msg.MATH_ROUND_OPERATOR_ROUNDDOWN, 'ROUNDDOWN']
+ ]
+ },
+ {
+ "type": "input_value",
+ "name": "NUM",
+ "check": Blockly.Types.DECIMAL.checkList
+ }
+ ],
+ "output": Blockly.Types.DECIMAL.output,
+ "colour": Blockly.Blocks.math.HUE,
+ "tooltip": Blockly.Msg.MATH_ROUND_TOOLTIP,
+ "helpUrl": Blockly.Msg.MATH_ROUND_HELPURL
+ });
+ },
+ /** @return {!string} Type of the block, round always returns a float. */
+ getBlockType: function() {
+ return Blockly.Types.DECIMAL;
+ }
+};
+
+Blockly.Blocks['math_on_list'] = {
+ /**
+ * Block for evaluating a list of numbers to return sum, average, min, max,
+ * etc. Some functions also work on text (min, max, mode, median).
+ * @this Blockly.Block
+ */
+ init: function() {
+ var OPERATORS =
+ [[Blockly.Msg.MATH_ONLIST_OPERATOR_SUM, 'SUM'],
+ [Blockly.Msg.MATH_ONLIST_OPERATOR_MIN, 'MIN'],
+ [Blockly.Msg.MATH_ONLIST_OPERATOR_MAX, 'MAX'],
+ [Blockly.Msg.MATH_ONLIST_OPERATOR_AVERAGE, 'AVERAGE'],
+ [Blockly.Msg.MATH_ONLIST_OPERATOR_MEDIAN, 'MEDIAN'],
+ [Blockly.Msg.MATH_ONLIST_OPERATOR_MODE, 'MODE'],
+ [Blockly.Msg.MATH_ONLIST_OPERATOR_STD_DEV, 'STD_DEV'],
+ [Blockly.Msg.MATH_ONLIST_OPERATOR_RANDOM, 'RANDOM']];
+ // Assign 'this' to a variable for use in the closures below.
+ var thisBlock = this;
+ this.setHelpUrl(Blockly.Msg.MATH_ONLIST_HELPURL);
+ this.setColour(Blockly.Blocks.math.HUE);
+ this.setOutput(true, Blockly.Types.NUMBER.output);
+ var dropdown = new Blockly.FieldDropdown(OPERATORS, function(newOp) {
+ thisBlock.updateType_(newOp);
+ });
+ this.appendValueInput('LIST')
+ .setCheck(Blockly.Types.ARRAY.checkList)
+ .appendField(dropdown, 'OP');
+ this.setTooltip(function() {
+ var mode = thisBlock.getFieldValue('OP');
+ var TOOLTIPS = {
+ 'SUM': Blockly.Msg.MATH_ONLIST_TOOLTIP_SUM,
+ 'MIN': Blockly.Msg.MATH_ONLIST_TOOLTIP_MIN,
+ 'MAX': Blockly.Msg.MATH_ONLIST_TOOLTIP_MAX,
+ 'AVERAGE': Blockly.Msg.MATH_ONLIST_TOOLTIP_AVERAGE,
+ 'MEDIAN': Blockly.Msg.MATH_ONLIST_TOOLTIP_MEDIAN,
+ 'MODE': Blockly.Msg.MATH_ONLIST_TOOLTIP_MODE,
+ 'STD_DEV': Blockly.Msg.MATH_ONLIST_TOOLTIP_STD_DEV,
+ 'RANDOM': Blockly.Msg.MATH_ONLIST_TOOLTIP_RANDOM
+ };
+ return TOOLTIPS[mode];
+ });
+ },
+ /**
+ * Modify this block to have the correct output type.
+ * @param {string} newOp Either 'MODE' or some op than returns a number.
+ * @private
+ * @this Blockly.Block
+ */
+ updateType_: function(newOp) {
+ if (newOp == 'MODE') {
+ this.outputConnection.setCheck(Blockly.Types.ARRAY.output);
+ } else {
+ this.outputConnection.setCheck(Blockly.Types.NUMBER.output);
+ }
+ },
+ /**
+ * Create XML to represent the output type.
+ * @return {Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ container.setAttribute('op', this.getFieldValue('OP'));
+ return container;
+ },
+ /**
+ * Parse XML to restore the output type.
+ * @param {!Element} xmlElement XML storage element.
+ * @this Blockly.Block
+ */
+ domToMutation: function(xmlElement) {
+ this.updateType_(xmlElement.getAttribute('op'));
+ }
+ //TODO: a getBlockType once the list code is finished.
+};
+
+Blockly.Blocks['math_modulo'] = {
+ /**
+ * Block for remainder of a division.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.MATH_MODULO_TITLE,
+ "args0": [
+ {
+ "type": "input_value",
+ "name": "DIVIDEND",
+ "check": Blockly.Types.NUMBER.checkList
+ },
+ {
+ "type": "input_value",
+ "name": "DIVISOR",
+ "check": Blockly.Types.NUMBER.checkList
+ }
+ ],
+ "inputsInline": true,
+ "output": Blockly.Types.NUMBER.output,
+ "colour": Blockly.Blocks.math.HUE,
+ "tooltip": Blockly.Msg.MATH_MODULO_TOOLTIP,
+ "helpUrl": Blockly.Msg.MATH_MODULO_HELPURL
+ });
+ },
+ /** @return {!string} Type of the block, modulus only works on integers. */
+ getBlockType: function() {
+ //TODO: Right now the block inputs are set to integer but will accept the
+ // "compatible" type float or plain "number", need to fix to integer.
+ return Blockly.Types.NUMBER;
+ }
+};
+
+Blockly.Blocks['math_constrain'] = {
+ /**
+ * Block for constraining a number between two limits.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.MATH_CONSTRAIN_TITLE,
+ "args0": [
+ {
+ "type": "input_value",
+ "name": "VALUE",
+ "check": Blockly.Types.NUMBER.checkList
+ },
+ {
+ "type": "input_value",
+ "name": "LOW",
+ "check": Blockly.Types.NUMBER.checkList
+ },
+ {
+ "type": "input_value",
+ "name": "HIGH",
+ "check": Blockly.Types.NUMBER.checkList
+ }
+ ],
+ "inputsInline": true,
+ "output": Blockly.Types.NUMBER.output,
+ "colour": Blockly.Blocks.math.HUE,
+ "tooltip": Blockly.Msg.MATH_CONSTRAIN_TOOLTIP,
+ "helpUrl": Blockly.Msg.MATH_CONSTRAIN_HELPURL
+ });
+ }
+ //TODO: a getBlockType of the same type as the inputs.
+};
+
+Blockly.Blocks['math_random_int'] = {
+ /**
+ * Block for random integer between [X] and [Y].
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.MATH_RANDOM_INT_TITLE,
+ "args0": [
+ {
+ "type": "input_value",
+ "name": "FROM",
+ "check": Blockly.Types.NUMBER.checkList
+ },
+ {
+ "type": "input_value",
+ "name": "TO",
+ "check": Blockly.Types.NUMBER.checkList
+ }
+ ],
+ "inputsInline": true,
+ "output": Blockly.Types.NUMBER.output,
+ "colour": Blockly.Blocks.math.HUE,
+ "tooltip": Blockly.Msg.MATH_RANDOM_INT_TOOLTIP,
+ "helpUrl": Blockly.Msg.MATH_RANDOM_INT_HELPURL
+ });
+ },
+ /** @return {!string} Type of the block, by definition always an integer. */
+ getBlockType: function() {
+ return Blockly.Types.NUMBER;
+ }
+};
+
+Blockly.Blocks['math_random_float'] = {
+ /**
+ * Block for random fraction between 0 and 1.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.MATH_RANDOM_FLOAT_TITLE_RANDOM,
+ "output": Blockly.Types.DECIMAL.output,
+ "colour": Blockly.Blocks.math.HUE,
+ "tooltip": Blockly.Msg.MATH_RANDOM_FLOAT_TOOLTIP,
+ "helpUrl": Blockly.Msg.MATH_RANDOM_FLOAT_HELPURL
+ });
+ },
+ /** @return {!string} Type of the block, by definition always a float. */
+ getBlockType: function() {
+ return Blockly.Types.DECIMAL;
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/procedures.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/procedures.js
new file mode 100644
index 000000000..61f7eb315
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/procedures.js
@@ -0,0 +1,870 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2012 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Procedure blocks for Blockly.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.procedures');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/**
+ * Common HSV hue for all blocks in this category.
+ */
+Blockly.Blocks.procedures.HUE = 290;
+
+Blockly.Blocks['procedures_defnoreturn'] = {
+ /**
+ * Block for defining a procedure with no return value.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var nameField = new Blockly.FieldTextInput(
+ Blockly.Msg.PROCEDURES_DEFNORETURN_PROCEDURE,
+ Blockly.Procedures.rename);
+ nameField.setSpellcheck(false);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_TITLE)
+ .appendField(nameField, 'NAME')
+ .appendField('', 'PARAMS');
+ //this.setMutator(new Blockly.Mutator(['procedures_mutatorarg']));
+ if (Blockly.Msg.PROCEDURES_DEFNORETURN_COMMENT) {
+ this.setCommentText(Blockly.Msg.PROCEDURES_DEFNORETURN_COMMENT);
+ }
+ this.setColour(Blockly.Blocks.procedures.HUE);
+ this.setTooltip(Blockly.Msg.PROCEDURES_DEFNORETURN_TOOLTIP);
+ this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFNORETURN_HELPURL);
+ this.arguments_ = [];
+ this.setStatements_(true);
+ this.statementConnection_ = null;
+ },
+ /**
+ * Initialization of the block has completed, clean up anything that may be
+ * inconsistent as a result of the XML loading.
+ * @this Blockly.Block
+ */
+ validate: function () {
+ var name = Blockly.Procedures.findLegalName(
+ this.getFieldValue('NAME'), this);
+ this.setFieldValue(name, 'NAME');
+ },
+ /**
+ * Add or remove the statement block from this function definition.
+ * @param {boolean} hasStatements True if a statement block is needed.
+ * @this Blockly.Block
+ */
+ setStatements_: function(hasStatements) {
+ if (this.hasStatements_ === hasStatements) {
+ return;
+ }
+ if (hasStatements) {
+ this.appendStatementInput('STACK')
+ .appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_DO);
+ 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.
+ * Display a warning if there are duplicately named parameters.
+ * @private
+ * @this Blockly.Block
+ */
+ updateParams_: function() {
+ // Check for duplicated arguments.
+ var badArg = false;
+ var hash = {};
+ for (var i = 0; i < this.arguments_.length; i++) {
+ if (hash['arg_' + this.arguments_[i].toLowerCase()]) {
+ badArg = true;
+ break;
+ }
+ hash['arg_' + this.arguments_[i].toLowerCase()] = true;
+ }
+ if (badArg) {
+ this.setWarningText(Blockly.Msg.PROCEDURES_DEF_DUPLICATE_WARNING);
+ } else {
+ this.setWarningText(null);
+ }
+ // Merge the arguments into a human-readable list.
+ var paramString = '';
+ if (this.arguments_.length) {
+ paramString = Blockly.Msg.PROCEDURES_BEFORE_PARAMS +
+ ' ' + this.arguments_.join(', ');
+ }
+ // The params field is deterministic based on the mutation,
+ // no need to fire a change event.
+ Blockly.Events.disable();
+ this.setFieldValue(paramString, 'PARAMS');
+ Blockly.Events.enable();
+ },
+ /**
+ * Create XML to represent the argument inputs.
+ * @param {=boolean} opt_paramIds 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: function(opt_paramIds) {
+ var container = document.createElement('mutation');
+ if (opt_paramIds) {
+ container.setAttribute('name', this.getFieldValue('NAME'));
+ }
+ for (var i = 0; i < this.arguments_.length; i++) {
+ var parameter = document.createElement('arg');
+ parameter.setAttribute('name', this.arguments_[i]);
+ if (opt_paramIds && 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: function(xmlElement) {
+ this.arguments_ = [];
+ for (var i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) {
+ if (childNode.nodeName.toLowerCase() == 'arg') {
+ this.arguments_.push(childNode.getAttribute('name'));
+ }
+ }
+ this.updateParams_();
+ Blockly.Procedures.mutateCallers(this);
+
+ // Show or hide the statement input.
+ this.setStatements_(xmlElement.getAttribute('statements') !== 'false');
+ },
+ /**
+ * Populate the mutator's dialog with this block's components.
+ * @param {!Blockly.Workspace} workspace Mutator's workspace.
+ * @return {!Blockly.Block} Root block in mutator.
+ * @this Blockly.Block
+ */
+ decompose: function(workspace) {
+ var containerBlock = workspace.newBlock('procedures_mutatorcontainer');
+ containerBlock.initSvg();
+
+ // Check/uncheck the allow statement box.
+ if (this.getInput('RETURN')) {
+ containerBlock.setFieldValue(this.hasStatements_ ? 'TRUE' : 'FALSE',
+ 'STATEMENTS');
+ } else {
+ containerBlock.getInput('STATEMENT_INPUT').setVisible(false);
+ }
+
+ // Parameter list.
+ var connection = containerBlock.getInput('STACK').connection;
+ for (var i = 0; i < this.arguments_.length; i++) {
+ var paramBlock = workspace.newBlock('procedures_mutatorarg');
+ paramBlock.initSvg();
+ paramBlock.setFieldValue(this.arguments_[i], 'NAME');
+ // Store the old location.
+ paramBlock.oldLocation = i;
+ connection.connect(paramBlock.previousConnection);
+ connection = paramBlock.nextConnection;
+ }
+ // Initialize procedure's callers with blank IDs.
+ Blockly.Procedures.mutateCallers(this);
+ return containerBlock;
+ },
+ /**
+ * Reconfigure this block based on the mutator dialog's components.
+ * @param {!Blockly.Block} containerBlock Root block in mutator.
+ * @this Blockly.Block
+ */
+ compose: function(containerBlock) {
+ // Parameter list.
+ this.arguments_ = [];
+ this.paramIds_ = [];
+ var paramBlock = containerBlock.getInputTargetBlock('STACK');
+ while (paramBlock) {
+ this.arguments_.push(paramBlock.getFieldValue('NAME'));
+ this.paramIds_.push(paramBlock.id);
+ paramBlock = paramBlock.nextConnection &&
+ paramBlock.nextConnection.targetBlock();
+ }
+ this.updateParams_();
+ Blockly.Procedures.mutateCallers(this);
+
+ // Show/hide the statement input.
+ var hasStatements = containerBlock.getFieldValue('STATEMENTS');
+ if (hasStatements !== null) {
+ hasStatements = hasStatements == 'TRUE';
+ if (this.hasStatements_ != hasStatements) {
+ if (hasStatements) {
+ this.setStatements_(true);
+ // Restore the stack, if one was saved.
+ Blockly.Mutator.reconnect(this.statementConnection_, this, 'STACK');
+ this.statementConnection_ = null;
+ } else {
+ // Save the stack, then disconnect it.
+ var stackConnection = this.getInput('STACK').connection;
+ this.statementConnection_ = stackConnection.targetConnection;
+ if (this.statementConnection_) {
+ var stackBlock = stackConnection.targetBlock();
+ stackBlock.unplug();
+ stackBlock.bumpNeighbours_();
+ }
+ this.setStatements_(false);
+ }
+ }
+ }
+ },
+ /**
+ * Dispose of any callers.
+ * @this Blockly.Block
+ */
+ dispose: function() {
+ var name = this.getFieldValue('NAME');
+ Blockly.Procedures.disposeCallers(name, this.workspace);
+ // Call parent's destructor.
+ this.constructor.prototype.dispose.apply(this, arguments);
+ },
+ /**
+ * 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: function() {
+ return [this.getFieldValue('NAME'), this.arguments_, false];
+ },
+ /**
+ * Return all variables referenced by this block.
+ * @return {!Array.} List of variable names.
+ * @this Blockly.Block
+ */
+ getVars: function() {
+ return this.arguments_;
+ },
+ /**
+ * Notification that a variable is renaming.
+ * If the name matches one of this block's variables, rename it.
+ * @param {string} oldName Previous name of variable.
+ * @param {string} newName Renamed variable.
+ * @this Blockly.Block
+ */
+ renameVar: function(oldName, newName) {
+ var change = false;
+ for (var i = 0; i < this.arguments_.length; i++) {
+ if (Blockly.Names.equals(oldName, this.arguments_[i])) {
+ this.arguments_[i] = newName;
+ change = true;
+ }
+ }
+ if (change) {
+ this.updateParams_();
+ // Update the mutator's variables if the mutator is open.
+ if (this.mutator.isVisible()) {
+ var blocks = this.mutator.workspace_.getAllBlocks();
+ for (var i = 0, block; block = blocks[i]; i++) {
+ if (block.type == 'procedures_mutatorarg' &&
+ Blockly.Names.equals(oldName, block.getFieldValue('NAME'))) {
+ block.setFieldValue(newName, 'NAME');
+ }
+ }
+ }
+ }
+ },
+ /**
+ * Add custom menu options to this block's context menu.
+ * @param {!Array} options List of menu options to add to.
+ * @this Blockly.Block
+ */
+ customContextMenu: function(options) {
+ // Add option to create caller.
+ var option = {enabled: true};
+ var name = this.getFieldValue('NAME');
+ option.text = Blockly.Msg.PROCEDURES_CREATE_DO.replace('%1', name);
+ var xmlMutation = goog.dom.createDom('mutation');
+ xmlMutation.setAttribute('name', name);
+ for (var i = 0; i < this.arguments_.length; i++) {
+ var xmlArg = goog.dom.createDom('arg');
+ xmlArg.setAttribute('name', this.arguments_[i]);
+ xmlMutation.appendChild(xmlArg);
+ }
+ var xmlBlock = goog.dom.createDom('block', null, xmlMutation);
+ xmlBlock.setAttribute('type', this.callType_);
+ option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock);
+ options.push(option);
+
+ // Add options to create getters for each parameter.
+ if (!this.isCollapsed()) {
+ for (var i = 0; i < this.arguments_.length; i++) {
+ var option = {enabled: true};
+ var name = this.arguments_[i];
+ option.text = Blockly.Msg.VARIABLES_SET_CREATE_GET.replace('%1', name);
+ var xmlField = goog.dom.createDom('field', null, name);
+ xmlField.setAttribute('name', 'VAR');
+ var xmlBlock = goog.dom.createDom('block', null, xmlField);
+ xmlBlock.setAttribute('type', 'variables_get');
+ option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock);
+ options.push(option);
+ }
+ }
+ },
+ callType_: 'procedures_callnoreturn',
+ /** @return {!string} This block does not define type, so 'undefined' */
+ getVarType: function(varName) {
+ return Blockly.Types.UNDEF;
+ },
+ /** Contains the type of the arguments added with mutators. */
+ argsTypes: {},
+ /**
+ * Searches through a list of variables with type to assign the type of the
+ * arguments.
+ * @this Blockly.Block
+ * @param {Array} existingVars Associative array variable already
+ * defined, names as key, type as value.
+ */
+ setArgsType: function(existingVars) {
+ var varNames = this.arguments_;
+
+ // Check if variable has been defined already and save type
+ for (var name in existingVars) {
+ for (var i = 0, length_ = varNames.length; i < length_; i++) {
+ if (name === varNames[i]) {
+ this.argsTypes[name] = existingVars[name];
+ }
+ }
+ }
+ },
+ /**
+ * Retrieves the type of the arguments, types defined at setArgsType.
+ * @this Blockly.Block
+ * @return {string} Type of the argument indicated in the input.
+ */
+ getArgType: function(varName) {
+ for (var name in this.argsTypes) {
+ if (name == varName) {
+ return this.argsTypes[varName];
+ }
+ }
+ return null;
+ }
+};
+
+Blockly.Blocks['procedures_defreturn'] = {
+ /**
+ * Block for defining a procedure with a return value.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var nameField = new Blockly.FieldTextInput(
+ Blockly.Msg.PROCEDURES_DEFRETURN_PROCEDURE,
+ Blockly.Procedures.rename);
+ nameField.setSpellcheck(false);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_TITLE)
+ .appendField(nameField, 'NAME')
+ .appendField('', 'PARAMS');
+ this.appendValueInput('RETURN')
+ .setAlign(Blockly.ALIGN_RIGHT)
+ .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
+ //this.setMutator(new Blockly.Mutator(['procedures_mutatorarg']));
+ if (Blockly.Msg.PROCEDURES_DEFRETURN_COMMENT) {
+ this.setCommentText(Blockly.Msg.PROCEDURES_DEFRETURN_COMMENT);
+ }
+ this.setColour(Blockly.Blocks.procedures.HUE);
+ this.setTooltip(Blockly.Msg.PROCEDURES_DEFRETURN_TOOLTIP);
+ this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFRETURN_HELPURL);
+ this.arguments_ = [];
+ this.setStatements_(true);
+ this.statementConnection_ = null;
+ },
+ setStatements_: Blockly.Blocks['procedures_defnoreturn'].setStatements_,
+ validate: Blockly.Blocks['procedures_defnoreturn'].validate,
+ updateParams_: Blockly.Blocks['procedures_defnoreturn'].updateParams_,
+ mutationToDom: Blockly.Blocks['procedures_defnoreturn'].mutationToDom,
+ domToMutation: Blockly.Blocks['procedures_defnoreturn'].domToMutation,
+ decompose: Blockly.Blocks['procedures_defnoreturn'].decompose,
+ compose: Blockly.Blocks['procedures_defnoreturn'].compose,
+ dispose: Blockly.Blocks['procedures_defnoreturn'].dispose,
+ /**
+ * 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: function() {
+ return [this.getFieldValue('NAME'), this.arguments_, true];
+ },
+ getVars: Blockly.Blocks['procedures_defnoreturn'].getVars,
+ renameVar: Blockly.Blocks['procedures_defnoreturn'].renameVar,
+ customContextMenu: Blockly.Blocks['procedures_defnoreturn'].customContextMenu,
+ callType_: 'procedures_callreturn',
+ getVarType: Blockly.Blocks['procedures_defnoreturn'].getVarType,
+ argsTypes: {},
+ setArgsType: Blockly.Blocks['procedures_defnoreturn'].setArgsType,
+ getArgType: Blockly.Blocks['procedures_defnoreturn'].getArgType,
+ /**
+ * Searches through the nested blocks in the return input to find a variable
+ * type or returns NULL.
+ * @this Blockly.Block
+ * @return {string} String to indicate the type or NULL.
+ */
+ getReturnType: function() {
+ var returnType = Blockly.Types.NULL;
+ var returnBlock = this.getInputTargetBlock('RETURN');
+ if (returnBlock) {
+ // First check if the block itself has a type already
+ if (returnBlock.getBlockType) {
+ returnType = returnBlock.getBlockType();
+ } else {
+ returnType = Blockly.Types.getChildBlockType(returnBlock);
+ }
+ }
+ return returnType;
+ }
+};
+
+Blockly.Blocks['procedures_mutatorcontainer'] = {
+ /**
+ * Mutator block for procedure container.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TITLE);
+ this.appendStatementInput('STACK');
+ this.appendDummyInput('STATEMENT_INPUT')
+ .appendField(Blockly.Msg.PROCEDURES_ALLOW_STATEMENTS)
+ .appendField(new Blockly.FieldCheckbox('TRUE'), 'STATEMENTS');
+ this.setColour(Blockly.Blocks.procedures.HUE);
+ this.setTooltip(Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TOOLTIP);
+ this.contextMenu = false;
+ }
+};
+
+Blockly.Blocks['procedures_mutatorarg'] = {
+ /**
+ * Mutator block for procedure argument.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.PROCEDURES_MUTATORARG_TITLE)
+ .appendField(new Blockly.FieldTextInput('x', this.validator_), 'NAME');
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setColour(Blockly.Blocks.procedures.HUE);
+ this.setTooltip(Blockly.Msg.PROCEDURES_MUTATORARG_TOOLTIP);
+ this.contextMenu = false;
+ },
+ /**
+ * Obtain a valid name for the procedure.
+ * Merge runs of whitespace. Strip leading and trailing whitespace.
+ * Beyond this, all names are legal.
+ * @param {string} newVar User-supplied name.
+ * @return {?string} Valid name, or null if a name was not specified.
+ * @private
+ * @this Blockly.Block
+ */
+ validator_: function(newVar) {
+ newVar = newVar.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, '');
+ return newVar || null;
+ }
+};
+
+Blockly.Blocks['procedures_callnoreturn'] = {
+ /**
+ * Block for calling a procedure with no return value.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.appendDummyInput('TOPROW')
+ .appendField(this.id, 'NAME');
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setColour(Blockly.Blocks.procedures.HUE);
+ // Tooltip is set in renameProcedure.
+ this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLNORETURN_HELPURL);
+ this.arguments_ = [];
+ this.quarkConnections_ = {};
+ this.quarkIds_ = null;
+ },
+ /**
+ * Returns the name of the procedure this block calls.
+ * @return {string} Procedure name.
+ * @this Blockly.Block
+ */
+ getProcedureCall: function() {
+ // 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: function(oldName, newName) {
+ if (Blockly.Names.equals(oldName, this.getProcedureCall())) {
+ this.setFieldValue(newName, 'NAME');
+ this.setTooltip(
+ (this.outputConnection ? Blockly.Msg.PROCEDURES_CALLRETURN_TOOLTIP :
+ Blockly.Msg.PROCEDURES_CALLNORETURN_TOOLTIP)
+ .replace('%1', newName));
+ }
+ },
+ /**
+ * Notification that the procedure's parameters have changed.
+ * @param {!Array.} paramNames New param names, e.g. ['x', 'y', 'z'].
+ * @param {!Array.} paramIds IDs of params (consistent for each
+ * parameter through the life of a mutator, regardless of param renaming),
+ * e.g. ['piua', 'f8b_', 'oi.o'].
+ * @private
+ * @this Blockly.Block
+ */
+ setProcedureParameters_: function(paramNames, paramIds) {
+ // Data structures:
+ // this.arguments = ['x', 'y']
+ // Existing param names.
+ // this.quarkConnections_ {piua: null, f8b_: Blockly.Connection}
+ // Look-up of paramIds to connections plugged into the call block.
+ // this.quarkIds_ = ['piua', 'f8b_']
+ // Existing param IDs.
+ // Note that quarkConnections_ may include IDs that no longer exist, but
+ // which might reappear if a param is reattached in the mutator.
+ var defBlock = Blockly.Procedures.getDefinition(this.getProcedureCall(),
+ this.workspace);
+ var mutatorOpen = defBlock && defBlock.mutator &&
+ defBlock.mutator.isVisible();
+ if (!mutatorOpen) {
+ this.quarkConnections_ = {};
+ this.quarkIds_ = null;
+ }
+ if (!paramIds) {
+ // Reset the quarks (a mutator is about to open).
+ return;
+ }
+ if (goog.array.equals(this.arguments_, paramNames)) {
+ // No change.
+ this.quarkIds_ = paramIds;
+ return;
+ }
+ if (paramIds.length != paramNames.length) {
+ throw 'Error: paramNames and paramIds must be the same length.';
+ }
+ this.setCollapsed(false);
+ if (!this.quarkIds_) {
+ // Initialize tracking for this block.
+ this.quarkConnections_ = {};
+ if (paramNames.join('\n') == this.arguments_.join('\n')) {
+ // No change to the parameters, allow quarkConnections_ to be
+ // populated with the existing connections.
+ this.quarkIds_ = paramIds;
+ } else {
+ this.quarkIds_ = [];
+ }
+ }
+ // Switch off rendering while the block is rebuilt.
+ var savedRendered = this.rendered;
+ this.rendered = false;
+ // Update the quarkConnections_ with existing connections.
+ for (var i = 0; i < this.arguments_.length; i++) {
+ var input = this.getInput('ARG' + i);
+ if (input) {
+ var connection = input.connection.targetConnection;
+ this.quarkConnections_[this.quarkIds_[i]] = connection;
+ if (mutatorOpen && connection &&
+ paramIds.indexOf(this.quarkIds_[i]) == -1) {
+ // This connection should no longer be attached to this block.
+ connection.disconnect();
+ connection.getSourceBlock().bumpNeighbours_();
+ }
+ }
+ }
+ // Rebuild the block's arguments.
+ this.arguments_ = [].concat(paramNames);
+ this.updateShape_();
+ this.quarkIds_ = paramIds;
+ // Reconnect any child blocks.
+ if (this.quarkIds_) {
+ for (var i = 0; i < this.arguments_.length; i++) {
+ var quarkId = this.quarkIds_[i];
+ if (quarkId in this.quarkConnections_) {
+ var connection = this.quarkConnections_[quarkId];
+ if (!Blockly.Mutator.reconnect(connection, this, 'ARG' + i)) {
+ // Block no longer exists or has been attached elsewhere.
+ delete this.quarkConnections_[quarkId];
+ }
+ }
+ }
+ }
+ // Restore rendering and show the changes.
+ this.rendered = savedRendered;
+ if (this.rendered) {
+ this.render();
+ }
+ },
+ /**
+ * Modify this block to have the correct number of arguments.
+ * @private
+ * @this Blockly.Block
+ */
+ updateShape_: function() {
+ for (var i = 0; i < this.arguments_.length; i++) {
+ var 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();
+ field.setValue(this.arguments_[i]);
+ Blockly.Events.enable();
+ } else {
+ // Add new input.
+ field = new Blockly.FieldLabel(this.arguments_[i]);
+ var input = this.appendValueInput('ARG' + i)
+ .setAlign(Blockly.ALIGN_RIGHT)
+ .appendField(field, 'ARGNAME' + i);
+ input.init();
+ }
+ }
+ // Remove deleted inputs.
+ while (this.getInput('ARG' + i)) {
+ this.removeInput('ARG' + i);
+ i++;
+ }
+ // Add 'with:' if there are parameters, remove otherwise.
+ var topRow = this.getInput('TOPROW');
+ if (topRow) {
+ if (this.arguments_.length) {
+ if (!this.getField('WITH')) {
+ topRow.appendField(Blockly.Msg.PROCEDURES_CALL_BEFORE_PARAMS, '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: function() {
+ var container = document.createElement('mutation');
+ container.setAttribute('name', this.getProcedureCall());
+ for (var i = 0; i < this.arguments_.length; i++) {
+ var parameter = document.createElement('arg');
+ parameter.setAttribute('name', this.arguments_[i]);
+ 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: function(xmlElement) {
+ var name = xmlElement.getAttribute('name');
+ this.renameProcedure(this.getProcedureCall(), name);
+ var args = [];
+ var paramIds = [];
+ for (var i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) {
+ if (childNode.nodeName.toLowerCase() == 'arg') {
+ args.push(childNode.getAttribute('name'));
+ paramIds.push(childNode.getAttribute('paramId'));
+ }
+ }
+ this.setProcedureParameters_(args, paramIds);
+ },
+ /**
+ * Notification that a variable is renaming.
+ * If the name matches one of this block's variables, rename it.
+ * @param {string} oldName Previous name of variable.
+ * @param {string} newName Renamed variable.
+ * @this Blockly.Block
+ */
+ renameVar: function(oldName, newName) {
+ for (var i = 0; i < this.arguments_.length; i++) {
+ if (Blockly.Names.equals(oldName, this.arguments_[i])) {
+ this.arguments_[i] = newName;
+ this.getField('ARGNAME' + i).setValue(newName);
+ }
+ }
+ },
+ /**
+ * 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: function(options) {
+ var option = {enabled: true};
+ option.text = Blockly.Msg.PROCEDURES_HIGHLIGHT_DEF;
+ var name = this.getProcedureCall();
+ var workspace = this.workspace;
+ option.callback = function() {
+ var def = Blockly.Procedures.getDefinition(name, workspace);
+ def && def.select();
+ };
+ options.push(option);
+ }
+};
+
+Blockly.Blocks['procedures_callreturn'] = {
+ /**
+ * Block for calling a procedure with a return value.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.appendDummyInput('TOPROW')
+ .appendField('', 'NAME');
+ this.setOutput(true);
+ this.setColour(Blockly.Blocks.procedures.HUE);
+ // Tooltip is set in domToMutation.
+ this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLRETURN_HELPURL);
+ this.arguments_ = [];
+ this.quarkConnections_ = {};
+ this.quarkIds_ = null;
+ },
+ 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,
+ renameVar: Blockly.Blocks['procedures_callnoreturn'].renameVar,
+ customContextMenu:
+ Blockly.Blocks['procedures_callnoreturn'].customContextMenu,
+ /** @return {!string} Return value type from the function definition block. */
+ getBlockType: function() {
+ var defBlock = Blockly.Procedures.getDefinition(this.getProcedureCall(),
+ this.workspace);
+ return defBlock.getReturnType();
+ }
+};
+
+Blockly.Blocks['procedures_ifreturn'] = {
+ /**
+ * Block for conditionally returning a value from a procedure.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.appendValueInput('CONDITION')
+ .setCheck(Blockly.Types.BOOLEAN.checkList)
+ .appendField(Blockly.Msg.CONTROLS_IF_MSG_IF);
+ this.appendValueInput('VALUE')
+ .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
+ this.setInputsInline(true);
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setColour(Blockly.Blocks.procedures.HUE);
+ this.setTooltip(Blockly.Msg.PROCEDURES_IFRETURN_TOOLTIP);
+ this.setHelpUrl(Blockly.Msg.PROCEDURES_IFRETURN_HELPURL);
+ this.hasReturnValue_ = true;
+ },
+ /**
+ * Create XML to represent whether this block has a return value.
+ * @return {!Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var 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: function(xmlElement) {
+ var value = xmlElement.getAttribute('value');
+ this.hasReturnValue_ = (value == 1);
+ if (!this.hasReturnValue_) {
+ this.removeInput('VALUE');
+ this.appendDummyInput('VALUE')
+ .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
+ }
+ },
+ /**
+ * 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: function(e) {
+ var legal = false;
+ // Is the block nested in a procedure?
+ var block = this;
+ do {
+ if (this.FUNCTION_TYPES.indexOf(block.type) != -1) {
+ legal = true;
+ break;
+ }
+ block = block.getSurroundParent();
+ } while (block);
+ if (legal) {
+ // If needed, toggle whether this block has a return value.
+ if (block.type == 'procedures_defnoreturn' && this.hasReturnValue_) {
+ this.removeInput('VALUE');
+ this.appendDummyInput('VALUE')
+ .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
+ this.hasReturnValue_ = false;
+ } else if (block.type == 'procedures_defreturn' &&
+ !this.hasReturnValue_) {
+ this.removeInput('VALUE');
+ this.appendValueInput('VALUE')
+ .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
+ this.hasReturnValue_ = true;
+ }
+ this.setWarningText(null);
+ } else {
+ this.setWarningText(Blockly.Msg.PROCEDURES_IFRETURN_WARNING);
+ }
+ },
+ /**
+ * 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']
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/text.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/text.js
new file mode 100644
index 000000000..59abdd0b5
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/text.js
@@ -0,0 +1,711 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2012 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Text blocks for Blockly.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.texts');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/**
+ * Common HSV hue for all blocks in this category.
+ */
+Blockly.Blocks.texts.HUE = 160;
+
+Blockly.Blocks['text'] = {
+ /**
+ * Block for text value.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl(Blockly.Msg.TEXT_TEXT_HELPURL);
+ this.setColour(Blockly.Blocks.texts.HUE);
+ this.appendDummyInput()
+ .appendField(this.newQuote_(true))
+ .appendField(new Blockly.FieldTextInput(''), 'TEXT')
+ .appendField(this.newQuote_(false));
+ this.setOutput(true, Blockly.Types.TEXT.output);
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ // Text block is trivial. Use tooltip of parent block if it exists.
+ this.setTooltip(function() {
+ var parent = thisBlock.getParent();
+ return (parent && parent.getInputsInline() && parent.tooltip) ||
+ Blockly.Msg.TEXT_TEXT_TOOLTIP;
+ });
+ },
+ /**
+ * Create an image of an open or closed quote.
+ * @param {boolean} open True if open quote, false if closed.
+ * @return {!Blockly.FieldImage} The field image of the quote.
+ * @this Blockly.Block
+ * @private
+ */
+ newQuote_: function(open) {
+ if (open == this.RTL) {
+ var file = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAqUlEQVQI1z3KvUpCcRiA8ef9E4JNHhI0aFEacm1o0BsI0Slx8wa8gLauoDnoBhq7DcfWhggONDmJJgqCPA7neJ7p934EOOKOnM8Q7PDElo/4x4lFb2DmuUjcUzS3URnGib9qaPNbuXvBO3sGPHJDRG6fGVdMSeWDP2q99FQdFrz26Gu5Tq7dFMzUvbXy8KXeAj57cOklgA+u1B5AoslLtGIHQMaCVnwDnADZIFIrXsoXrgAAAABJRU5ErkJggg==';
+ } else {
+ var file = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAn0lEQVQI1z3OMa5BURSF4f/cQhAKjUQhuQmFNwGJEUi0RKN5rU7FHKhpjEH3TEMtkdBSCY1EIv8r7nFX9e29V7EBAOvu7RPjwmWGH/VuF8CyN9/OAdvqIXYLvtRaNjx9mMTDyo+NjAN1HNcl9ZQ5oQMM3dgDUqDo1l8DzvwmtZN7mnD+PkmLa+4mhrxVA9fRowBWmVBhFy5gYEjKMfz9AylsaRRgGzvZAAAAAElFTkSuQmCC';
+ }
+ return new Blockly.FieldImage(file, 12, 12, '"');
+ },
+ /** @return {!string} Type of the block, text block always a string. */
+ getBlockType: function() {
+ return Blockly.Types.TEXT;
+ }
+};
+
+Blockly.Blocks['text_join'] = {
+ /**
+ * Block for creating a string made up of any number of elements of any type.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl(Blockly.Msg.TEXT_JOIN_HELPURL);
+ this.setColour(Blockly.Blocks.texts.HUE);
+ this.itemCount_ = 2;
+ this.updateShape_();
+ this.setOutput(true, Blockly.Types.TEXT.output);
+ this.setMutator(new Blockly.Mutator(['text_create_join_item']));
+ this.setTooltip(Blockly.Msg.TEXT_JOIN_TOOLTIP);
+ },
+ /**
+ * Create XML to represent number of text inputs.
+ * @return {!Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ container.setAttribute('items', this.itemCount_);
+ return container;
+ },
+ /**
+ * Parse XML to restore the text inputs.
+ * @param {!Element} xmlElement XML storage element.
+ * @this Blockly.Block
+ */
+ domToMutation: function(xmlElement) {
+ this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10);
+ this.updateShape_();
+ },
+ /**
+ * Populate the mutator's dialog with this block's components.
+ * @param {!Blockly.Workspace} workspace Mutator's workspace.
+ * @return {!Blockly.Block} Root block in mutator.
+ * @this Blockly.Block
+ */
+ decompose: function(workspace) {
+ var containerBlock = workspace.newBlock('text_create_join_container');
+ containerBlock.initSvg();
+ var connection = containerBlock.getInput('STACK').connection;
+ for (var i = 0; i < this.itemCount_; i++) {
+ var itemBlock = workspace.newBlock('text_create_join_item');
+ itemBlock.initSvg();
+ connection.connect(itemBlock.previousConnection);
+ connection = itemBlock.nextConnection;
+ }
+ return containerBlock;
+ },
+ /**
+ * Reconfigure this block based on the mutator dialog's components.
+ * @param {!Blockly.Block} containerBlock Root block in mutator.
+ * @this Blockly.Block
+ */
+ compose: function(containerBlock) {
+ var itemBlock = containerBlock.getInputTargetBlock('STACK');
+ // Count number of inputs.
+ var connections = [];
+ while (itemBlock) {
+ connections.push(itemBlock.valueConnection_);
+ itemBlock = itemBlock.nextConnection &&
+ itemBlock.nextConnection.targetBlock();
+ }
+ // Disconnect any children that don't belong.
+ for (var i = 0; i < this.itemCount_; i++) {
+ var connection = this.getInput('ADD' + i).connection.targetConnection;
+ if (connection && connections.indexOf(connection) == -1) {
+ connection.disconnect();
+ }
+ }
+ this.itemCount_ = connections.length;
+ this.updateShape_();
+ // Reconnect any child blocks.
+ for (var i = 0; i < this.itemCount_; i++) {
+ Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i);
+ }
+ },
+ /**
+ * Store pointers to any connected child blocks.
+ * @param {!Blockly.Block} containerBlock Root block in mutator.
+ * @this Blockly.Block
+ */
+ saveConnections: function(containerBlock) {
+ var itemBlock = containerBlock.getInputTargetBlock('STACK');
+ var i = 0;
+ while (itemBlock) {
+ var input = this.getInput('ADD' + i);
+ itemBlock.valueConnection_ = input && input.connection.targetConnection;
+ i++;
+ itemBlock = itemBlock.nextConnection &&
+ itemBlock.nextConnection.targetBlock();
+ }
+ },
+ /**
+ * Modify this block to have the correct number of inputs.
+ * @private
+ * @this Blockly.Block
+ */
+ updateShape_: function() {
+ if (this.itemCount_ && this.getInput('EMPTY')) {
+ this.removeInput('EMPTY');
+ } else if (!this.itemCount_ && !this.getInput('EMPTY')) {
+ this.appendDummyInput('EMPTY')
+ .appendField(this.newQuote_(true))
+ .appendField(this.newQuote_(false));
+ }
+ // Add new inputs.
+ for (var i = 0; i < this.itemCount_; i++) {
+ if (!this.getInput('ADD' + i)) {
+ var input = this.appendValueInput('ADD' + i);
+ if (i == 0) {
+ input.appendField(Blockly.Msg.TEXT_JOIN_TITLE_CREATEWITH);
+ }
+ }
+ }
+ // Remove deleted inputs.
+ while (this.getInput('ADD' + i)) {
+ this.removeInput('ADD' + i);
+ i++;
+ }
+ },
+ newQuote_: Blockly.Blocks['text'].newQuote_,
+ /** @return {!string} Type of the block, text join always a string. */
+ getBlockType: function() {
+ return Blockly.Types.TEXT;
+ }
+};
+
+Blockly.Blocks['text_create_join_container'] = {
+ /**
+ * Mutator block for container.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setColour(Blockly.Blocks.texts.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.TEXT_CREATE_JOIN_TITLE_JOIN);
+ this.appendStatementInput('STACK');
+ this.setTooltip(Blockly.Msg.TEXT_CREATE_JOIN_TOOLTIP);
+ this.contextMenu = false;
+ }
+};
+
+Blockly.Blocks['text_create_join_item'] = {
+ /**
+ * Mutator block for add items.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setColour(Blockly.Blocks.texts.HUE);
+ this.appendDummyInput()
+ .appendField(Blockly.Msg.TEXT_CREATE_JOIN_ITEM_TITLE_ITEM);
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setTooltip(Blockly.Msg.TEXT_CREATE_JOIN_ITEM_TOOLTIP);
+ this.contextMenu = false;
+ }
+};
+
+Blockly.Blocks['text_append'] = {
+ /**
+ * Block for appending to a variable in place.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl(Blockly.Msg.TEXT_APPEND_HELPURL);
+ this.setColour(Blockly.Blocks.texts.HUE);
+ this.appendValueInput('TEXT')
+ .appendField(Blockly.Msg.TEXT_APPEND_TO)
+ .appendField(new Blockly.FieldVariable(
+ Blockly.Msg.TEXT_APPEND_VARIABLE), 'VAR')
+ .appendField(Blockly.Msg.TEXT_APPEND_APPENDTEXT);
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ // Assign 'this' to a variable for use in the tooltip closure below.
+ var thisBlock = this;
+ this.setTooltip(function() {
+ return Blockly.Msg.TEXT_APPEND_TOOLTIP.replace('%1',
+ thisBlock.getFieldValue('VAR'));
+ });
+ },
+ /**
+ * Set's the type of the variable selected in the drop down list. As there is
+ * only one possible option, the variable input is not really checked.
+ * @param {!string} varName Name of the variable to check type.
+ * @return {string} String to indicate the variable type.
+ */
+ getVarType: function(varName) {
+ return Blockly.Types.TEXT;
+ }
+};
+
+Blockly.Blocks['text_length'] = {
+ /**
+ * Block for string length.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.TEXT_LENGTH_TITLE,
+ "args0": [
+ {
+ "type": "input_value",
+ "name": "VALUE",
+ "check": Blockly.Types.TEXT.checkList.concat('Array'),
+ }
+ ],
+ "output": Blockly.Types.NUMBER.output,
+ "colour": Blockly.Blocks.texts.HUE,
+ "tooltip": Blockly.Msg.TEXT_LENGTH_TOOLTIP,
+ "helpUrl": Blockly.Msg.TEXT_LENGTH_HELPURL
+ });
+ },
+ /** @return {!string} Type of the block, text length always an integer. */
+ getBlockType: function() {
+ return Blockly.Types.NUMBER;
+ }
+};
+
+Blockly.Blocks['text_isEmpty'] = {
+ /**
+ * Block for is the string null?
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.TEXT_ISEMPTY_TITLE,
+ "args0": [
+ {
+ "type": "input_value",
+ "name": "VALUE",
+ "check": Blockly.Types.TEXT.checkList.concat('Array'),
+ }
+ ],
+ "output": Blockly.Types.BOOLEAN.output,
+ "colour": Blockly.Blocks.texts.HUE,
+ "tooltip": Blockly.Msg.TEXT_ISEMPTY_TOOLTIP,
+ "helpUrl": Blockly.Msg.TEXT_ISEMPTY_HELPURL
+ });
+ },
+ /** @return {!string} Type of the block, check always returns a boolean. */
+ getBlockType: function() {
+ return Blockly.Types.BOOLEAN;
+ }
+};
+
+Blockly.Blocks['text_indexOf'] = {
+ /**
+ * Block for finding a substring in the text.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var OPERATORS =
+ [[Blockly.Msg.TEXT_INDEXOF_OPERATOR_FIRST, 'FIRST'],
+ [Blockly.Msg.TEXT_INDEXOF_OPERATOR_LAST, 'LAST']];
+ this.setHelpUrl(Blockly.Msg.TEXT_INDEXOF_HELPURL);
+ this.setColour(Blockly.Blocks.texts.HUE);
+ this.setOutput(true, Blockly.Types.NUMBER.output);
+ this.appendValueInput('VALUE')
+ .setCheck(Blockly.Types.TEXT.checkList)
+ .appendField(Blockly.Msg.TEXT_INDEXOF_INPUT_INTEXT);
+ this.appendValueInput('FIND')
+ .setCheck(Blockly.Types.TEXT.checkList)
+ .appendField(new Blockly.FieldDropdown(OPERATORS), 'END');
+ if (Blockly.Msg.TEXT_INDEXOF_TAIL) {
+ this.appendDummyInput().appendField(Blockly.Msg.TEXT_INDEXOF_TAIL);
+ }
+ this.setInputsInline(true);
+ this.setTooltip(Blockly.Msg.TEXT_INDEXOF_TOOLTIP);
+ }
+};
+
+Blockly.Blocks['text_charAt'] = {
+ /**
+ * Block for getting a character from the string.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.WHERE_OPTIONS =
+ [[Blockly.Msg.TEXT_CHARAT_FROM_START, 'FROM_START'],
+ [Blockly.Msg.TEXT_CHARAT_FROM_END, 'FROM_END'],
+ [Blockly.Msg.TEXT_CHARAT_FIRST, 'FIRST'],
+ [Blockly.Msg.TEXT_CHARAT_LAST, 'LAST'],
+ [Blockly.Msg.TEXT_CHARAT_RANDOM, 'RANDOM']];
+ this.setHelpUrl(Blockly.Msg.TEXT_CHARAT_HELPURL);
+ this.setColour(Blockly.Blocks.texts.HUE);
+ this.setOutput(true, Blockly.Types.TEXT.output);
+ this.appendValueInput('VALUE')
+ .setCheck(Blockly.Types.TEXT.checkList)
+ .appendField(Blockly.Msg.TEXT_CHARAT_INPUT_INTEXT);
+ this.appendDummyInput('AT');
+ this.setInputsInline(true);
+ this.updateAt_(true);
+ this.setTooltip(Blockly.Msg.TEXT_CHARAT_TOOLTIP);
+ },
+ /**
+ * Create XML to represent whether there is an 'AT' input.
+ * @return {!Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ var isAt = this.getInput('AT').type == Blockly.INPUT_VALUE;
+ container.setAttribute('at', isAt);
+ return container;
+ },
+ /**
+ * Parse XML to restore the 'AT' input.
+ * @param {!Element} xmlElement XML storage element.
+ * @this Blockly.Block
+ */
+ domToMutation: function(xmlElement) {
+ // Note: Until January 2013 this block did not have mutations,
+ // so 'at' defaults to true.
+ var isAt = (xmlElement.getAttribute('at') != 'false');
+ this.updateAt_(isAt);
+ },
+ /**
+ * Create or delete an input for the numeric index.
+ * @param {boolean} isAt True if the input should exist.
+ * @private
+ * @this Blockly.Block
+ */
+ updateAt_: function(isAt) {
+ // Destroy old 'AT' and 'ORDINAL' inputs.
+ this.removeInput('AT');
+ this.removeInput('ORDINAL', true);
+ // Create either a value 'AT' input or a dummy input.
+ if (isAt) {
+ this.appendValueInput('AT').setCheck(Blockly.Types.NUMBER.checkList);
+ if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) {
+ this.appendDummyInput('ORDINAL')
+ .appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX);
+ }
+ } else {
+ this.appendDummyInput('AT');
+ }
+ if (Blockly.Msg.TEXT_CHARAT_TAIL) {
+ this.removeInput('TAIL', true);
+ this.appendDummyInput('TAIL')
+ .appendField(Blockly.Msg.TEXT_CHARAT_TAIL);
+ }
+ var menu = new Blockly.FieldDropdown(this.WHERE_OPTIONS, function(value) {
+ var newAt = (value == 'FROM_START') || (value == 'FROM_END');
+ // The 'isAt' variable is available due to this function being a closure.
+ if (newAt != isAt) {
+ var block = this.sourceBlock_;
+ block.updateAt_(newAt);
+ // This menu has been destroyed and replaced. Update the replacement.
+ block.setFieldValue(value, 'WHERE');
+ return null;
+ }
+ return undefined;
+ });
+ this.getInput('AT').appendField(menu, 'WHERE');
+ }
+};
+
+Blockly.Blocks['text_getSubstring'] = {
+ /**
+ * Block for getting substring.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this['WHERE_OPTIONS_1'] =
+ [[Blockly.Msg.TEXT_GET_SUBSTRING_START_FROM_START, 'FROM_START'],
+ [Blockly.Msg.TEXT_GET_SUBSTRING_START_FROM_END, 'FROM_END'],
+ [Blockly.Msg.TEXT_GET_SUBSTRING_START_FIRST, 'FIRST']];
+ this['WHERE_OPTIONS_2'] =
+ [[Blockly.Msg.TEXT_GET_SUBSTRING_END_FROM_START, 'FROM_START'],
+ [Blockly.Msg.TEXT_GET_SUBSTRING_END_FROM_END, 'FROM_END'],
+ [Blockly.Msg.TEXT_GET_SUBSTRING_END_LAST, 'LAST']];
+ this.setHelpUrl(Blockly.Msg.TEXT_GET_SUBSTRING_HELPURL);
+ this.setColour(Blockly.Blocks.texts.HUE);
+ this.appendValueInput('STRING')
+ .setCheck(Blockly.Types.TEXT.checkList)
+ .appendField(Blockly.Msg.TEXT_GET_SUBSTRING_INPUT_IN_TEXT);
+ this.appendDummyInput('AT1');
+ this.appendDummyInput('AT2');
+ if (Blockly.Msg.TEXT_GET_SUBSTRING_TAIL) {
+ this.appendDummyInput('TAIL')
+ .appendField(Blockly.Msg.TEXT_GET_SUBSTRING_TAIL);
+ }
+ this.setInputsInline(true);
+ this.setOutput(true, Blockly.Types.TEXT.output);
+ this.updateAt_(1, true);
+ this.updateAt_(2, true);
+ this.setTooltip(Blockly.Msg.TEXT_GET_SUBSTRING_TOOLTIP);
+ },
+ /**
+ * Create XML to represent whether there are 'AT' inputs.
+ * @return {!Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ var isAt1 = this.getInput('AT1').type == Blockly.INPUT_VALUE;
+ container.setAttribute('at1', isAt1);
+ var isAt2 = this.getInput('AT2').type == Blockly.INPUT_VALUE;
+ container.setAttribute('at2', isAt2);
+ return container;
+ },
+ /**
+ * Parse XML to restore the 'AT' inputs.
+ * @param {!Element} xmlElement XML storage element.
+ * @this Blockly.Block
+ */
+ domToMutation: function(xmlElement) {
+ var isAt1 = (xmlElement.getAttribute('at1') == 'true');
+ var isAt2 = (xmlElement.getAttribute('at2') == 'true');
+ this.updateAt_(1, isAt1);
+ this.updateAt_(2, isAt2);
+ },
+ /**
+ * Create or delete an input for a numeric index.
+ * This block has two such inputs, independant of each other.
+ * @param {number} n Specify first or second input (1 or 2).
+ * @param {boolean} isAt True if the input should exist.
+ * @private
+ * @this Blockly.Block
+ */
+ updateAt_: function(n, isAt) {
+ // Create or delete an input for the numeric index.
+ // Destroy old 'AT' and 'ORDINAL' inputs.
+ this.removeInput('AT' + n);
+ this.removeInput('ORDINAL' + n, true);
+ // Create either a value 'AT' input or a dummy input.
+ if (isAt) {
+ this.appendValueInput('AT' + n).setCheck(Blockly.Types.NUMBER.checkList);
+ if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) {
+ this.appendDummyInput('ORDINAL' + n)
+ .appendField(Blockly.Msg.ORDINAL_NUMBER_SUFFIX);
+ }
+ } else {
+ this.appendDummyInput('AT' + n);
+ }
+ // Move tail, if present, to end of block.
+ if (n == 2 && Blockly.Msg.TEXT_GET_SUBSTRING_TAIL) {
+ this.removeInput('TAIL', true);
+ this.appendDummyInput('TAIL')
+ .appendField(Blockly.Msg.TEXT_GET_SUBSTRING_TAIL);
+ }
+ var menu = new Blockly.FieldDropdown(this['WHERE_OPTIONS_' + n],
+ function(value) {
+ var newAt = (value == 'FROM_START') || (value == 'FROM_END');
+ // The 'isAt' variable is available due to this function being a closure.
+ if (newAt != isAt) {
+ var block = this.sourceBlock_;
+ block.updateAt_(n, newAt);
+ // This menu has been destroyed and replaced. Update the replacement.
+ block.setFieldValue(value, 'WHERE' + n);
+ return null;
+ }
+ return undefined;
+ });
+ this.getInput('AT' + n)
+ .appendField(menu, 'WHERE' + n);
+ if (n == 1) {
+ this.moveInputBefore('AT1', 'AT2');
+ }
+ }
+};
+
+Blockly.Blocks['text_changeCase'] = {
+ /**
+ * Block for changing capitalization.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var OPERATORS =
+ [[Blockly.Msg.TEXT_CHANGECASE_OPERATOR_UPPERCASE, 'UPPERCASE'],
+ [Blockly.Msg.TEXT_CHANGECASE_OPERATOR_LOWERCASE, 'LOWERCASE'],
+ [Blockly.Msg.TEXT_CHANGECASE_OPERATOR_TITLECASE, 'TITLECASE']];
+ this.setHelpUrl(Blockly.Msg.TEXT_CHANGECASE_HELPURL);
+ this.setColour(Blockly.Blocks.texts.HUE);
+ this.appendValueInput('TEXT')
+ .setCheck(Blockly.Types.TEXT.checkList)
+ .appendField(new Blockly.FieldDropdown(OPERATORS), 'CASE');
+ this.setOutput(true, Blockly.Types.TEXT.output);
+ this.setTooltip(Blockly.Msg.TEXT_CHANGECASE_TOOLTIP);
+ }
+};
+
+Blockly.Blocks['text_trim'] = {
+ /**
+ * Block for trimming spaces.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var OPERATORS =
+ [[Blockly.Msg.TEXT_TRIM_OPERATOR_BOTH, 'BOTH'],
+ [Blockly.Msg.TEXT_TRIM_OPERATOR_LEFT, 'LEFT'],
+ [Blockly.Msg.TEXT_TRIM_OPERATOR_RIGHT, 'RIGHT']];
+ this.setHelpUrl(Blockly.Msg.TEXT_TRIM_HELPURL);
+ this.setColour(Blockly.Blocks.texts.HUE);
+ this.appendValueInput('TEXT')
+ .setCheck(Blockly.Types.TEXT.checkList)
+ .appendField(new Blockly.FieldDropdown(OPERATORS), 'MODE');
+ this.setOutput(true, Blockly.Types.TEXT.output);
+ this.setTooltip(Blockly.Msg.TEXT_TRIM_TOOLTIP);
+ },
+ /** Assigns a type to the block, trim always takes and returns a string. */
+ getBlockType: function() {
+ return Blockly.Types.TEXT;
+ }
+};
+
+Blockly.Blocks['text_print'] = {
+ /**
+ * Block for print statement.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.TEXT_PRINT_TITLE,
+ "args0": [
+ {
+ "type": "input_value",
+ "name": "TEXT"
+ }
+ ],
+ "previousStatement": null,
+ "nextStatement": null,
+ "colour": Blockly.Blocks.texts.HUE,
+ "tooltip": Blockly.Msg.TEXT_PRINT_TOOLTIP,
+ "helpUrl": Blockly.Msg.TEXT_PRINT_HELPURL
+ });
+ }
+};
+
+Blockly.Blocks['text_prompt_ext'] = {
+ /**
+ * Block for prompt function (external message).
+ * @this Blockly.Block
+ */
+ init: function() {
+ var TYPES =
+ [[Blockly.Msg.TEXT_PROMPT_TYPE_TEXT, Blockly.Types.TEXT.output],
+ [Blockly.Msg.TEXT_PROMPT_TYPE_NUMBER, Blockly.Types.NUMBER.output]];
+ this.setHelpUrl(Blockly.Msg.TEXT_PROMPT_HELPURL);
+ this.setColour(Blockly.Blocks.texts.HUE);
+ // Assign 'this' to a variable for use in the closures below.
+ var thisBlock = this;
+ var dropdown = new Blockly.FieldDropdown(TYPES, function(newOp) {
+ thisBlock.updateType_(newOp);
+ });
+ this.appendValueInput('TEXT')
+ .appendField(dropdown, 'TYPE');
+ this.setOutput(true, Blockly.Types.TEXT.output);
+ this.setTooltip(function() {
+ return (thisBlock.getFieldValue('TYPE') == Blockly.Types.TEXT.output) ?
+ Blockly.Msg.TEXT_PROMPT_TOOLTIP_TEXT :
+ Blockly.Msg.TEXT_PROMPT_TOOLTIP_NUMBER;
+ });
+ },
+ /**
+ * Modify this block to have the correct output type.
+ * @param {string} newOp Either 'TEXT' or 'NUMBER'.
+ * @private
+ * @this Blockly.Block
+ */
+ updateType_: function(newOp) {
+ this.outputConnection.setCheck(newOp == Blockly.Types.NUMBER.output ?
+ Blockly.Types.NUMBER.checkList : Blockly.Types.TEXT.checkList);
+ },
+ /**
+ * Create XML to represent the output type.
+ * @return {!Element} XML storage element.
+ * @this Blockly.Block
+ */
+ mutationToDom: function() {
+ var container = document.createElement('mutation');
+ container.setAttribute('type', this.getFieldValue('TYPE'));
+ return container;
+ },
+ /**
+ * Parse XML to restore the output type.
+ * @param {!Element} xmlElement XML storage element.
+ * @this Blockly.Block
+ */
+ domToMutation: function(xmlElement) {
+ this.updateType_(xmlElement.getAttribute('type'));
+ },
+ /** @return {!string} Type of the block, prompt always returns a string. */
+ getBlockType: function() {
+ return (this.getFieldValue('TYPE') == Blockly.Types.TEXT.output) ?
+ Blockly.Types.TEXT : Blockly.Types.NUMBER;
+ }
+};
+
+Blockly.Blocks['text_prompt'] = {
+ /**
+ * Block for prompt function (internal message).
+ * The 'text_prompt_ext' block is preferred as it is more flexible.
+ * @this Blockly.Block
+ */
+ init: function() {
+ var TYPES =
+ [[Blockly.Msg.TEXT_PROMPT_TYPE_TEXT, Blockly.Types.TEXT.output],
+ [Blockly.Msg.TEXT_PROMPT_TYPE_NUMBER, Blockly.Types.NUMBER.output]];
+ // Assign 'this' to a variable for use in the closure below.
+ var thisBlock = this;
+ this.setHelpUrl(Blockly.Msg.TEXT_PROMPT_HELPURL);
+ this.setColour(Blockly.Blocks.texts.HUE);
+ var dropdown = new Blockly.FieldDropdown(TYPES, function(newOp) {
+ thisBlock.updateType_(newOp);
+ });
+ this.appendDummyInput()
+ .appendField(dropdown, 'TYPE')
+ .appendField(this.newQuote_(true))
+ .appendField(new Blockly.FieldTextInput(''), 'TEXT')
+ .appendField(this.newQuote_(false));
+ this.setOutput(true, Blockly.Types.TEXT.output);
+ this.setTooltip(function() {
+ return (thisBlock.getFieldValue('TYPE') == Blockly.Types.TEXT.output) ?
+ Blockly.Msg.TEXT_PROMPT_TOOLTIP_TEXT :
+ Blockly.Msg.TEXT_PROMPT_TOOLTIP_NUMBER;
+ });
+ },
+ newQuote_: Blockly.Blocks['text'].newQuote_,
+ updateType_: Blockly.Blocks['text_prompt_ext'].updateType_,
+ mutationToDom: Blockly.Blocks['text_prompt_ext'].mutationToDom,
+ domToMutation: Blockly.Blocks['text_prompt_ext'].domToMutation,
+ /** Assigns a type to the block, prompt always returns a string. */
+ getBlockType: function() {
+ return (this.getFieldValue('TYPE') == Blockly.Types.NUMBER.output) ?
+ Blockly.Types.NUMBER : Blockly.Types.TEXT;
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/variables.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/variables.js
new file mode 100644
index 000000000..96f0a05dd
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/blocks/variables.js
@@ -0,0 +1,127 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2012 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Variable blocks for Blockly.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+goog.provide('Blockly.Blocks.variables');
+
+goog.require('Blockly.Blocks');
+goog.require('Blockly.Types');
+
+
+/**
+ * Common HSV hue for all blocks in this category.
+ */
+Blockly.Blocks.variables.HUE = 330;
+
+Blockly.Blocks['variables_get'] = {
+ /**
+ * Block for variable getter.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.setHelpUrl(Blockly.Msg.VARIABLES_GET_HELPURL);
+ this.setColour(Blockly.Blocks.variables.HUE);
+ this.appendDummyInput()
+ .appendField(new Blockly.FieldVariable(
+ Blockly.Msg.VARIABLES_DEFAULT_NAME), 'VAR');
+ this.setOutput(true);
+ this.setTooltip(Blockly.Msg.VARIABLES_GET_TOOLTIP);
+ this.contextMenuMsg_ = Blockly.Msg.VARIABLES_GET_CREATE_SET;
+ },
+ contextMenuType_: 'variables_set',
+ /**
+ * Add menu option to create getter/setter block for this setter/getter.
+ * @param {!Array} options List of menu options to add to.
+ * @this Blockly.Block
+ */
+ customContextMenu: function(options) {
+ var option = {enabled: true};
+ var name = this.getFieldValue('VAR');
+ option.text = this.contextMenuMsg_.replace('%1', name);
+ var xmlField = goog.dom.createDom('field', null, name);
+ xmlField.setAttribute('name', 'VAR');
+ var xmlBlock = goog.dom.createDom('block', null, xmlField);
+ xmlBlock.setAttribute('type', this.contextMenuType_);
+ option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock);
+ options.push(option);
+ },
+ /**
+ * @return {!string} Retrieves the type (stored in varType) of this block.
+ * @this Blockly.Block
+ */
+ getBlockType: function() {
+ return [Blockly.Types.UNDEF, this.getFieldValue('VAR')];
+ },
+ /**
+ * Gets the stored type of the variable indicated in the argument. As only one
+ * variable is stored in this block, no need to check input
+ * @this Blockly.
+ * @param {!string} varName Name of this block variable to check type.
+ * @return {!string} String to indicate the type of this block.
+ */
+ getVarType: function(varName) {
+ return [Blockly.Types.UNDEF, this.getFieldValue('VAR')];
+ },
+};
+
+Blockly.Blocks['variables_set'] = {
+ /**
+ * Block for variable setter.
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": Blockly.Msg.VARIABLES_SET,
+ "args0": [
+ {
+ "type": "field_variable",
+ "name": "VAR",
+ "variable": Blockly.Msg.VARIABLES_DEFAULT_NAME
+ },
+ {
+ "type": "input_value",
+ "name": "VALUE"
+ }
+ ],
+ "previousStatement": null,
+ "nextStatement": null,
+ "colour": Blockly.Blocks.variables.HUE,
+ "tooltip": Blockly.Msg.VARIABLES_SET_TOOLTIP,
+ "helpUrl": Blockly.Msg.VARIABLES_SET_HELPURL
+ });
+ this.contextMenuMsg_ = Blockly.Msg.VARIABLES_SET_CREATE_GET;
+ },
+ contextMenuType_: 'variables_get',
+ customContextMenu: Blockly.Blocks['variables_get'].customContextMenu,
+ /**
+ * Searches through the nested blocks to find a variable type.
+ * @this Blockly.Block
+ * @param {!string} varName Name of this block variable to check type.
+ * @return {string} String to indicate the type of this block.
+ */
+ getVarType: function(varName) {
+ return Blockly.Types.getChildBlockType(this);
+ }
+};
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/factory.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/factory.js
new file mode 100644
index 000000000..375ad1386
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/factory.js
@@ -0,0 +1,805 @@
+/**
+ * Blockly Demos: Block Factory
+ *
+ * Copyright 2012 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview JavaScript for Blockly's Block Factory application.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+/**
+ * Workspace for user to build block.
+ * @type {Blockly.Workspace}
+ */
+var mainWorkspace = null;
+
+/**
+ * Workspace for preview of block.
+ * @type {Blockly.Workspace}
+ */
+var previewWorkspace = null;
+
+/**
+ * Name of block if not named.
+ */
+var UNNAMED = 'unnamed';
+
+/**
+ * Change the language code format.
+ */
+function formatChange() {
+ var mask = document.getElementById('blocklyMask');
+ var languagePre = document.getElementById('languagePre');
+ var languageTA = document.getElementById('languageTA');
+ if (document.getElementById('format').value == 'Manual') {
+ Blockly.hideChaff();
+ mask.style.display = 'block';
+ languagePre.style.display = 'none';
+ languageTA.style.display = 'block';
+ var code = languagePre.textContent.trim();
+ languageTA.value = code;
+ languageTA.focus();
+ updatePreview();
+ } else {
+ mask.style.display = 'none';
+ languageTA.style.display = 'none';
+ languagePre.style.display = 'block';
+ updateLanguage();
+ }
+ disableEnableLink();
+}
+
+/**
+ * Update the language code based on constructs made in Blockly.
+ */
+function updateLanguage() {
+ var rootBlock = getRootBlock();
+ if (!rootBlock) {
+ return;
+ }
+ var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase();
+ if (!blockType) {
+ blockType = UNNAMED;
+ }
+ blockType = blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1');
+ switch (document.getElementById('format').value) {
+ case 'JSON':
+ var code = formatJson_(blockType, rootBlock);
+ break;
+ case 'JavaScript':
+ var code = formatJavaScript_(blockType, rootBlock);
+ break;
+ }
+ injectCode(code, 'languagePre');
+ updatePreview();
+}
+
+/**
+ * Update the language code as JSON.
+ * @param {string} blockType Name of block.
+ * @param {!Blockly.Block} rootBlock Factory_base block.
+ * @return {string} Generanted language code.
+ * @private
+ */
+function formatJson_(blockType, rootBlock) {
+ var JS = {};
+ // Type is not used by Blockly, but may be used by a loader.
+ JS.type = blockType;
+ // Generate inputs.
+ var message = [];
+ var args = [];
+ var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
+ var lastInput = null;
+ while (contentsBlock) {
+ if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
+ var fields = getFieldsJson_(contentsBlock.getInputTargetBlock('FIELDS'));
+ for (var i = 0; i < fields.length; i++) {
+ if (typeof fields[i] == 'string') {
+ message.push(fields[i].replace(/%/g, '%%'));
+ } else {
+ args.push(fields[i]);
+ message.push('%' + args.length);
+ }
+ }
+
+ var input = {type: contentsBlock.type};
+ // Dummy inputs don't have names. Other inputs do.
+ if (contentsBlock.type != 'input_dummy') {
+ input.name = contentsBlock.getFieldValue('INPUTNAME');
+ }
+ var check = JSON.parse(getOptTypesFrom(contentsBlock, 'TYPE') || 'null');
+ if (check) {
+ input.check = check;
+ }
+ var align = contentsBlock.getFieldValue('ALIGN');
+ if (align != 'LEFT') {
+ input.align = align;
+ }
+ args.push(input);
+ message.push('%' + args.length);
+ lastInput = contentsBlock;
+ }
+ contentsBlock = contentsBlock.nextConnection &&
+ contentsBlock.nextConnection.targetBlock();
+ }
+ // Remove last input if dummy and not empty.
+ if (lastInput && lastInput.type == 'input_dummy') {
+ var fields = lastInput.getInputTargetBlock('FIELDS');
+ if (fields && getFieldsJson_(fields).join('').trim() != '') {
+ var align = lastInput.getFieldValue('ALIGN');
+ if (align != 'LEFT') {
+ JS.lastDummyAlign0 = align;
+ }
+ args.pop();
+ message.pop();
+ }
+ }
+ JS.message0 = message.join(' ');
+ if (args.length) {
+ JS.args0 = args;
+ }
+ // Generate inline/external switch.
+ if (rootBlock.getFieldValue('INLINE') == 'EXT') {
+ JS.inputsInline = false;
+ } else if (rootBlock.getFieldValue('INLINE') == 'INT') {
+ JS.inputsInline = true;
+ }
+ // Generate output, or next/previous connections.
+ switch (rootBlock.getFieldValue('CONNECTIONS')) {
+ case 'LEFT':
+ JS.output =
+ JSON.parse(getOptTypesFrom(rootBlock, 'OUTPUTTYPE') || 'null');
+ break;
+ case 'BOTH':
+ JS.previousStatement =
+ JSON.parse(getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
+ JS.nextStatement =
+ JSON.parse(getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
+ break;
+ case 'TOP':
+ JS.previousStatement =
+ JSON.parse(getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
+ break;
+ case 'BOTTOM':
+ JS.nextStatement =
+ JSON.parse(getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
+ break;
+ }
+ // Generate colour.
+ var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
+ if (colourBlock && !colourBlock.disabled) {
+ var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
+ JS.colour = hue;
+ }
+ JS.tooltip = '';
+ JS.helpUrl = 'http://www.example.com/';
+ return JSON.stringify(JS, null, ' ');
+}
+
+/**
+ * Update the language code as JavaScript.
+ * @param {string} blockType Name of block.
+ * @param {!Blockly.Block} rootBlock Factory_base block.
+ * @return {string} Generanted language code.
+ * @private
+ */
+function formatJavaScript_(blockType, rootBlock) {
+ var code = [];
+ code.push("Blockly.Blocks['" + blockType + "'] = {");
+ code.push(" init: function() {");
+ // Generate inputs.
+ var TYPES = {'input_value': 'appendValueInput',
+ 'input_statement': 'appendStatementInput',
+ 'input_dummy': 'appendDummyInput'};
+ var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
+ while (contentsBlock) {
+ if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
+ var name = '';
+ // Dummy inputs don't have names. Other inputs do.
+ if (contentsBlock.type != 'input_dummy') {
+ name = escapeString(contentsBlock.getFieldValue('INPUTNAME'));
+ }
+ code.push(' this.' + TYPES[contentsBlock.type] + '(' + name + ')');
+ var check = getOptTypesFrom(contentsBlock, 'TYPE');
+ if (check) {
+ code.push(' .setCheck(' + check + ')');
+ }
+ var align = contentsBlock.getFieldValue('ALIGN');
+ if (align != 'LEFT') {
+ code.push(' .setAlign(Blockly.ALIGN_' + align + ')');
+ }
+ var fields = getFieldsJs_(contentsBlock.getInputTargetBlock('FIELDS'));
+ for (var i = 0; i < fields.length; i++) {
+ code.push(' .appendField(' + fields[i] + ')');
+ }
+ // Add semicolon to last line to finish the statement.
+ code[code.length - 1] += ';';
+ }
+ contentsBlock = contentsBlock.nextConnection &&
+ contentsBlock.nextConnection.targetBlock();
+ }
+ // Generate inline/external switch.
+ if (rootBlock.getFieldValue('INLINE') == 'EXT') {
+ code.push(' this.setInputsInline(false);');
+ } else if (rootBlock.getFieldValue('INLINE') == 'INT') {
+ code.push(' this.setInputsInline(true);');
+ }
+ // Generate output, or next/previous connections.
+ switch (rootBlock.getFieldValue('CONNECTIONS')) {
+ case 'LEFT':
+ code.push(connectionLineJs_('setOutput', 'OUTPUTTYPE'));
+ break;
+ case 'BOTH':
+ code.push(connectionLineJs_('setPreviousStatement', 'TOPTYPE'));
+ code.push(connectionLineJs_('setNextStatement', 'BOTTOMTYPE'));
+ break;
+ case 'TOP':
+ code.push(connectionLineJs_('setPreviousStatement', 'TOPTYPE'));
+ break;
+ case 'BOTTOM':
+ code.push(connectionLineJs_('setNextStatement', 'BOTTOMTYPE'));
+ break;
+ }
+ // Generate colour.
+ var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
+ if (colourBlock && !colourBlock.disabled) {
+ var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
+ if (!isNaN(hue)) {
+ code.push(' this.setColour(' + hue + ');');
+ }
+ }
+ code.push(" this.setTooltip('');");
+ code.push(" this.setHelpUrl('http://www.example.com/');");
+ code.push(' }');
+ code.push('};');
+ return code.join('\n');
+}
+
+/**
+ * Create JS code required to create a top, bottom, or value connection.
+ * @param {string} functionName JavaScript function name.
+ * @param {string} typeName Name of type input.
+ * @return {string} Line of JavaScript code to create connection.
+ * @private
+ */
+function connectionLineJs_(functionName, typeName) {
+ var type = getOptTypesFrom(getRootBlock(), typeName);
+ if (type) {
+ type = ', ' + type;
+ } else {
+ type = '';
+ }
+ return ' this.' + functionName + '(true' + type + ');';
+}
+
+/**
+ * Returns field strings and any config.
+ * @param {!Blockly.Block} block Input block.
+ * @return {!Array.} Field strings.
+ * @private
+ */
+function getFieldsJs_(block) {
+ var fields = [];
+ while (block) {
+ if (!block.disabled && !block.getInheritedDisabled()) {
+ switch (block.type) {
+ case 'field_static':
+ // Result: 'hello'
+ fields.push(escapeString(block.getFieldValue('TEXT')));
+ break;
+ case 'field_input':
+ // Result: new Blockly.FieldTextInput('Hello'), 'GREET'
+ fields.push('new Blockly.FieldTextInput(' +
+ escapeString(block.getFieldValue('TEXT')) + '), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ break;
+ case 'field_angle':
+ // Result: new Blockly.FieldAngle(90), 'ANGLE'
+ fields.push('new Blockly.FieldAngle(' +
+ parseFloat(block.getFieldValue('ANGLE')) + '), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ break;
+ case 'field_checkbox':
+ // Result: new Blockly.FieldCheckbox('TRUE'), 'CHECK'
+ fields.push('new Blockly.FieldCheckbox(' +
+ escapeString(block.getFieldValue('CHECKED')) + '), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ break;
+ case 'field_colour':
+ // Result: new Blockly.FieldColour('#ff0000'), 'COLOUR'
+ fields.push('new Blockly.FieldColour(' +
+ escapeString(block.getFieldValue('COLOUR')) + '), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ break;
+ case 'field_date':
+ // Result: new Blockly.FieldDate('2015-02-04'), 'DATE'
+ fields.push('new Blockly.FieldDate(' +
+ escapeString(block.getFieldValue('DATE')) + '), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ break;
+ case 'field_variable':
+ // Result: new Blockly.FieldVariable('item'), 'VAR'
+ var varname = escapeString(block.getFieldValue('TEXT') || null);
+ fields.push('new Blockly.FieldVariable(' + varname + '), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ break;
+ case 'field_dropdown':
+ // Result:
+ // new Blockly.FieldDropdown([['yes', '1'], ['no', '0']]), 'TOGGLE'
+ var options = [];
+ for (var i = 0; i < block.optionCount_; i++) {
+ options[i] = '[' + escapeString(block.getFieldValue('USER' + i)) +
+ ', ' + escapeString(block.getFieldValue('CPU' + i)) + ']';
+ }
+ if (options.length) {
+ fields.push('new Blockly.FieldDropdown([' +
+ options.join(', ') + ']), ' +
+ escapeString(block.getFieldValue('FIELDNAME')));
+ }
+ break;
+ case 'field_image':
+ // Result: new Blockly.FieldImage('http://...', 80, 60)
+ var src = escapeString(block.getFieldValue('SRC'));
+ var width = Number(block.getFieldValue('WIDTH'));
+ var height = Number(block.getFieldValue('HEIGHT'));
+ var alt = escapeString(block.getFieldValue('ALT'));
+ fields.push('new Blockly.FieldImage(' +
+ src + ', ' + width + ', ' + height + ', ' + alt + ')');
+ break;
+ }
+ }
+ block = block.nextConnection && block.nextConnection.targetBlock();
+ }
+ return fields;
+}
+
+/**
+ * Returns field strings and any config.
+ * @param {!Blockly.Block} block Input block.
+ * @return {!Array.} Array of static text and field configs.
+ * @private
+ */
+function getFieldsJson_(block) {
+ var fields = [];
+ while (block) {
+ if (!block.disabled && !block.getInheritedDisabled()) {
+ switch (block.type) {
+ case 'field_static':
+ // Result: 'hello'
+ fields.push(block.getFieldValue('TEXT'));
+ break;
+ case 'field_input':
+ fields.push({
+ type: block.type,
+ name: block.getFieldValue('FIELDNAME'),
+ text: block.getFieldValue('TEXT')
+ });
+ break;
+ case 'field_angle':
+ fields.push({
+ type: block.type,
+ name: block.getFieldValue('FIELDNAME'),
+ angle: Number(block.getFieldValue('ANGLE'))
+ });
+ break;
+ case 'field_checkbox':
+ fields.push({
+ type: block.type,
+ name: block.getFieldValue('FIELDNAME'),
+ checked: block.getFieldValue('CHECKED') == 'TRUE'
+ });
+ break;
+ case 'field_colour':
+ fields.push({
+ type: block.type,
+ name: block.getFieldValue('FIELDNAME'),
+ colour: block.getFieldValue('COLOUR')
+ });
+ break;
+ case 'field_date':
+ fields.push({
+ type: block.type,
+ name: block.getFieldValue('FIELDNAME'),
+ date: block.getFieldValue('DATE')
+ });
+ break;
+ case 'field_variable':
+ fields.push({
+ type: block.type,
+ name: block.getFieldValue('FIELDNAME'),
+ variable: block.getFieldValue('TEXT') || null
+ });
+ break;
+ case 'field_dropdown':
+ var options = [];
+ for (var i = 0; i < block.optionCount_; i++) {
+ options[i] = [block.getFieldValue('USER' + i),
+ block.getFieldValue('CPU' + i)];
+ }
+ if (options.length) {
+ fields.push({
+ type: block.type,
+ name: block.getFieldValue('FIELDNAME'),
+ options: options
+ });
+ }
+ break;
+ case 'field_image':
+ fields.push({
+ type: block.type,
+ src: block.getFieldValue('SRC'),
+ width: Number(block.getFieldValue('WIDTH')),
+ height: Number(block.getFieldValue('HEIGHT')),
+ alt: block.getFieldValue('ALT')
+ });
+ break;
+ }
+ }
+ block = block.nextConnection && block.nextConnection.targetBlock();
+ }
+ return fields;
+}
+
+/**
+ * Escape a string.
+ * @param {string} string String to escape.
+ * @return {string} Escaped string surrouned by quotes.
+ */
+function escapeString(string) {
+ return JSON.stringify(string);
+}
+
+/**
+ * Fetch the type(s) defined in the given input.
+ * Format as a string for appending to the generated code.
+ * @param {!Blockly.Block} block Block with input.
+ * @param {string} name Name of the input.
+ * @return {?string} String defining the types.
+ */
+function getOptTypesFrom(block, name) {
+ var types = getTypesFrom_(block, name);
+ if (types.length == 0) {
+ return undefined;
+ } else if (types.indexOf('null') != -1) {
+ return 'null';
+ } else if (types.length == 1) {
+ return types[0];
+ } else {
+ return '[' + types.join(', ') + ']';
+ }
+}
+
+/**
+ * Fetch the type(s) defined in the given input.
+ * @param {!Blockly.Block} block Block with input.
+ * @param {string} name Name of the input.
+ * @return {!Array.} List of types.
+ * @private
+ */
+function getTypesFrom_(block, name) {
+ var typeBlock = block.getInputTargetBlock(name);
+ var types;
+ if (!typeBlock || typeBlock.disabled) {
+ types = [];
+ } else if (typeBlock.type == 'type_other') {
+ types = [escapeString(typeBlock.getFieldValue('TYPE'))];
+ } else if (typeBlock.type == 'type_group') {
+ types = [];
+ for (var n = 0; n < typeBlock.typeCount_; n++) {
+ types = types.concat(getTypesFrom_(typeBlock, 'TYPE' + n));
+ }
+ // Remove duplicates.
+ var hash = Object.create(null);
+ for (var n = types.length - 1; n >= 0; n--) {
+ if (hash[types[n]]) {
+ types.splice(n, 1);
+ }
+ hash[types[n]] = true;
+ }
+ } else {
+ types = [escapeString(typeBlock.valueType)];
+ }
+ return types;
+}
+
+/**
+ * Update the generator code.
+ * @param {!Blockly.Block} block Rendered block in preview workspace.
+ */
+function updateGenerator(block) {
+ function makeVar(root, name) {
+ name = name.toLowerCase().replace(/\W/g, '_');
+ return ' var ' + root + '_' + name;
+ }
+ var language = document.getElementById('language').value;
+ var code = [];
+ code.push("Blockly." + language + "['" + block.type +
+ "'] = function(block) {");
+
+ // Generate getters for any fields or inputs.
+ for (var i = 0, input; input = block.inputList[i]; i++) {
+ for (var j = 0, field; field = input.fieldRow[j]; j++) {
+ var name = field.name;
+ if (!name) {
+ continue;
+ }
+ if (field instanceof Blockly.FieldVariable) {
+ // Subclass of Blockly.FieldDropdown, must test first.
+ code.push(makeVar('variable', name) +
+ " = Blockly." + language +
+ ".variableDB_.getName(block.getFieldValue('" + name +
+ "'), Blockly.Variables.NAME_TYPE);");
+ } else if (field instanceof Blockly.FieldAngle) {
+ // Subclass of Blockly.FieldTextInput, must test first.
+ code.push(makeVar('angle', name) +
+ " = block.getFieldValue('" + name + "');");
+ } else if (Blockly.FieldDate && field instanceof Blockly.FieldDate) {
+ // Blockly.FieldDate may not be compiled into Blockly.
+ code.push(makeVar('date', name) +
+ " = block.getFieldValue('" + name + "');");
+ } else if (field instanceof Blockly.FieldColour) {
+ code.push(makeVar('colour', name) +
+ " = block.getFieldValue('" + name + "');");
+ } else if (field instanceof Blockly.FieldCheckbox) {
+ code.push(makeVar('checkbox', name) +
+ " = block.getFieldValue('" + name + "') == 'TRUE';");
+ } else if (field instanceof Blockly.FieldDropdown) {
+ code.push(makeVar('dropdown', name) +
+ " = block.getFieldValue('" + name + "');");
+ } else if (field instanceof Blockly.FieldTextInput) {
+ code.push(makeVar('text', name) +
+ " = block.getFieldValue('" + name + "');");
+ }
+ }
+ var name = input.name;
+ if (name) {
+ if (input.type == Blockly.INPUT_VALUE) {
+ code.push(makeVar('value', name) +
+ " = Blockly." + language + ".valueToCode(block, '" + name +
+ "', Blockly." + language + ".ORDER_ATOMIC);");
+ } else if (input.type == Blockly.NEXT_STATEMENT) {
+ code.push(makeVar('statements', name) +
+ " = Blockly." + language + ".statementToCode(block, '" +
+ name + "');");
+ }
+ }
+ }
+ // Most languages end lines with a semicolon. Python does not.
+ var lineEnd = {
+ 'JavaScript': ';',
+ 'Python': '',
+ 'PHP': ';',
+ 'Dart': ';'
+ };
+ code.push(" // TODO: Assemble " + language + " into code variable.");
+ if (block.outputConnection) {
+ code.push(" var code = '...';");
+ code.push(" // TODO: Change ORDER_NONE to the correct strength.");
+ code.push(" return [code, Blockly." + language + ".ORDER_NONE];");
+ } else {
+ code.push(" var code = '..." + (lineEnd[language] || '') + "\\n';");
+ code.push(" return code;");
+ }
+ code.push("};");
+
+ injectCode(code.join('\n'), 'generatorPre');
+}
+
+/**
+ * Existing direction ('ltr' vs 'rtl') of preview.
+ */
+var oldDir = null;
+
+/**
+ * Update the preview display.
+ */
+function updatePreview() {
+ // Toggle between LTR/RTL if needed (also used in first display).
+ var newDir = document.getElementById('direction').value;
+ if (oldDir != newDir) {
+ if (previewWorkspace) {
+ previewWorkspace.dispose();
+ }
+ var rtl = newDir == 'rtl';
+ previewWorkspace = Blockly.inject('preview',
+ {rtl: rtl,
+ media: '../../media/',
+ scrollbars: true});
+ oldDir = newDir;
+ }
+ previewWorkspace.clear();
+
+ // Fetch the code and determine its format (JSON or JavaScript).
+ var format = document.getElementById('format').value;
+ if (format == 'Manual') {
+ var code = document.getElementById('languageTA').value;
+ // If the code is JSON, it will parse, otherwise treat as JS.
+ try {
+ JSON.parse(code);
+ format = 'JSON';
+ } catch (e) {
+ format = 'JavaScript';
+ }
+ } else {
+ var code = document.getElementById('languagePre').textContent;
+ }
+ if (!code.trim()) {
+ // Nothing to render. Happens while cloud storage is loading.
+ return;
+ }
+
+ // Backup Blockly.Blocks object so that main workspace and preview don't
+ // collide if user creates a 'factory_base' block, for instance.
+ var backupBlocks = Blockly.Blocks;
+ try {
+ // Make a shallow copy.
+ Blockly.Blocks = {};
+ for (var prop in backupBlocks) {
+ Blockly.Blocks[prop] = backupBlocks[prop];
+ }
+
+ if (format == 'JSON') {
+ var json = JSON.parse(code);
+ Blockly.Blocks[json.id || UNNAMED] = {
+ init: function() {
+ this.jsonInit(json);
+ }
+ };
+ } else if (format == 'JavaScript') {
+ eval(code);
+ } else {
+ throw 'Unknown format: ' + format;
+ }
+
+ // Look for a block on Blockly.Blocks that does not match the backup.
+ var blockType = null;
+ for (var type in Blockly.Blocks) {
+ if (typeof Blockly.Blocks[type].init == 'function' &&
+ Blockly.Blocks[type] != backupBlocks[type]) {
+ blockType = type;
+ break;
+ }
+ }
+ if (!blockType) {
+ return;
+ }
+
+ // Create the preview block.
+ var previewBlock = previewWorkspace.newBlock(blockType);
+ previewBlock.initSvg();
+ previewBlock.render();
+ previewBlock.setMovable(false);
+ previewBlock.setDeletable(false);
+ previewBlock.moveBy(15, 10);
+ previewWorkspace.clearUndo();
+
+ updateGenerator(previewBlock);
+ } finally {
+ Blockly.Blocks = backupBlocks;
+ }
+}
+
+/**
+ * Inject code into a pre tag, with syntax highlighting.
+ * Safe from HTML/script injection.
+ * @param {string} code Lines of code.
+ * @param {string} id ID of element to inject into.
+ */
+function injectCode(code, id) {
+ var pre = document.getElementById(id);
+ pre.textContent = code;
+ code = pre.innerHTML;
+ code = prettyPrintOne(code, 'js');
+ pre.innerHTML = code;
+}
+
+/**
+ * Return the uneditable container block that everything else attaches to.
+ * @return {Blockly.Block}
+ */
+function getRootBlock() {
+ var blocks = mainWorkspace.getTopBlocks(false);
+ for (var i = 0, block; block = blocks[i]; i++) {
+ if (block.type == 'factory_base') {
+ return block;
+ }
+ }
+ return null;
+}
+
+/**
+ * Disable the link button if the format is 'Manual', enable otherwise.
+ */
+function disableEnableLink() {
+ var linkButton = document.getElementById('linkButton');
+ linkButton.disabled = document.getElementById('format').value == 'Manual';
+}
+
+/**
+ * Initialize Blockly and layout. Called on page load.
+ */
+function init() {
+ if ('BlocklyStorage' in window) {
+ BlocklyStorage.HTTPREQUEST_ERROR =
+ 'There was a problem with the request.\n';
+ BlocklyStorage.LINK_ALERT =
+ 'Share your blocks with this link:\n\n%1';
+ BlocklyStorage.HASH_ERROR =
+ 'Sorry, "%1" doesn\'t correspond with any saved Blockly file.';
+ BlocklyStorage.XML_ERROR = 'Could not load your saved file.\n'+
+ 'Perhaps it was created with a different version of Blockly?';
+ var linkButton = document.getElementById('linkButton');
+ linkButton.style.display = 'inline-block';
+ linkButton.addEventListener('click',
+ function() {BlocklyStorage.link(mainWorkspace);});
+ disableEnableLink();
+ }
+
+ document.getElementById('helpButton').addEventListener('click',
+ function() {
+ open('https://developers.google.com/blockly/custom-blocks/block-factory',
+ 'BlockFactoryHelp');
+ });
+
+ var expandList = [
+ document.getElementById('blockly'),
+ document.getElementById('blocklyMask'),
+ document.getElementById('preview'),
+ document.getElementById('languagePre'),
+ document.getElementById('languageTA'),
+ document.getElementById('generatorPre')
+ ];
+ var onresize = function(e) {
+ for (var i = 0, expand; expand = expandList[i]; i++) {
+ expand.style.width = (expand.parentNode.offsetWidth - 2) + 'px';
+ expand.style.height = (expand.parentNode.offsetHeight - 2) + 'px';
+ }
+ };
+ onresize();
+ window.addEventListener('resize', onresize);
+
+ var toolbox = document.getElementById('toolbox');
+ mainWorkspace = Blockly.inject('blockly',
+ {collapse: false,
+ toolbox: toolbox,
+ media: '../../media/'});
+
+ // Create the root block.
+ if ('BlocklyStorage' in window && window.location.hash.length > 1) {
+ BlocklyStorage.retrieveXml(window.location.hash.substring(1),
+ mainWorkspace);
+ } else {
+ var xml = '';
+ Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(xml), mainWorkspace);
+ }
+ mainWorkspace.clearUndo();
+
+ mainWorkspace.addChangeListener(updateLanguage);
+ document.getElementById('direction')
+ .addEventListener('change', updatePreview);
+ document.getElementById('languageTA')
+ .addEventListener('change', updatePreview);
+ document.getElementById('languageTA')
+ .addEventListener('keyup', updatePreview);
+ document.getElementById('format')
+ .addEventListener('change', formatChange);
+ document.getElementById('language')
+ .addEventListener('change', updatePreview);
+}
+window.addEventListener('load', init);
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/icon.png b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/icon.png
new file mode 100644
index 000000000..d4d19b457
Binary files /dev/null and b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/icon.png differ
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/index.html b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/index.html
new file mode 100644
index 000000000..0377bc587
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/index.html
@@ -0,0 +1,229 @@
+
+
+
+
+
+ Blockly Demo: Block Factory
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ Preview:
+
+
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+ Language code:
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+ Generator stub:
+
+
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 20
+ 65
+ 120
+ 160
+ 210
+ 230
+ 260
+ 290
+ 330
+
+
+
+
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/link.png b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/link.png
new file mode 100644
index 000000000..11dfd8284
Binary files /dev/null and b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/link.png differ
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/prettify.css b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/prettify.css
new file mode 100644
index 000000000..d44b3a228
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/prettify.css
@@ -0,0 +1 @@
+.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
\ No newline at end of file
diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/prettify.js b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/prettify.js
new file mode 100644
index 000000000..7b990496d
--- /dev/null
+++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/blockFactory/prettify.js
@@ -0,0 +1,30 @@
+!function(){var q=null;window.PR_SHOULD_USE_CONTINUATION=!0;
+(function(){function S(a){function d(e){var b=e.charCodeAt(0);if(b!==92)return b;var a=e.charAt(1);return(b=r[a])?b:"0"<=a&&a<="7"?parseInt(e.substring(1),8):a==="u"||a==="x"?parseInt(e.substring(2),16):e.charCodeAt(1)}function g(e){if(e<32)return(e<16?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e);return e==="\\"||e==="-"||e==="]"||e==="^"?"\\"+e:e}function b(e){var b=e.substring(1,e.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),e=[],a=
+b[0]==="^",c=["["];a&&c.push("^");for(var a=a?1:0,f=b.length;a122||(l<65||h>90||e.push([Math.max(65,h)|32,Math.min(l,90)|32]),l<97||h>122||e.push([Math.max(97,h)&-33,Math.min(l,122)&-33]))}}e.sort(function(e,a){return e[0]-a[0]||a[1]-e[1]});b=[];f=[];for(a=0;ah[0]&&(h[1]+1>h[0]&&c.push("-"),c.push(g(h[1])));c.push("]");return c.join("")}function s(e){for(var a=e.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),c=a.length,d=[],f=0,h=0;f=2&&e==="["?a[f]=b(l):e!=="\\"&&(a[f]=l.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return a.join("")}for(var x=0,m=!1,j=!1,k=0,c=a.length;k=5&&"lang-"===w.substring(0,5))&&!(t&&typeof t[1]==="string"))f=!1,w="src";f||(r[z]=w)}h=c;c+=z.length;if(f){f=t[1];var l=z.indexOf(f),B=l+f.length;t[2]&&(B=z.length-t[2].length,l=B-f.length);w=w.substring(5);H(j+h,z.substring(0,l),g,k);H(j+h+l,f,I(w,f),k);H(j+h+B,z.substring(B),g,k)}else k.push(j+h,w)}a.g=k}var b={},s;(function(){for(var g=a.concat(d),j=[],k={},c=0,i=g.length;c=0;)b[n.charAt(e)]=r;r=r[1];n=""+r;k.hasOwnProperty(n)||(j.push(r),k[n]=q)}j.push(/[\S\s]/);s=S(j)})();var x=d.length;return g}function v(a){var d=[],g=[];a.tripleQuotedStrings?d.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?d.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,
+q,"'\"`"]):d.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&g.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var b=a.hashComments;b&&(a.cStyleComments?(b>1?d.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):d.push(["com",/^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),g.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/,q])):d.push(["com",
+/^#[^\n\r]*/,q,"#"]));a.cStyleComments&&(g.push(["com",/^\/\/[^\n\r]*/,q]),g.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));if(b=a.regexLiterals){var s=(b=b>1?"":"\n\r")?".":"[\\S\\s]";g.push(["lang-regex",RegExp("^(?:^^\\.?|[+-]|[!=]=?=?|\\#|%=?|&&?=?|\\(|\\*=?|[+\\-]=|->|\\/=?|::?|<=?|>>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+("/(?=[^/*"+b+"])(?:[^/\\x5B\\x5C"+b+"]|\\x5C"+s+"|\\x5B(?:[^\\x5C\\x5D"+b+"]|\\x5C"+
+s+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&g.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&g.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),q]);d.push(["pln",/^\s+/,q," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");g.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,
+q],["pun",RegExp(b),q]);return C(d,g)}function J(a,d,g){function b(a){var c=a.nodeType;if(c==1&&!x.test(a.className))if("br"===a.nodeName)s(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&g){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(j.createTextNode(d),a.nextSibling),s(a),c||a.parentNode.removeChild(a)}}function s(a){function b(a,c){var d=
+c?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=b(e,1),g=a.nextSibling;e.appendChild(d);for(var i=g;i;i=g)g=i.nextSibling,e.appendChild(i)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var x=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,j=a.ownerDocument,k=j.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var c=[k],i=0;i=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*=l&&(b+=2);g>=B&&(r+=2)}}finally{if(f)f.style.display=h}}catch(u){D.console&&console.log(u&&u.stack||u)}}var D=window,y=["break,continue,do,else,for,if,return,while"],E=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
+"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],M=[E,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],N=[E,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],
+O=[N,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,internal,into,is,let,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var,virtual,where"],E=[E,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],P=[y,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],
+Q=[y,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],W=[y,"as,assert,const,copy,drop,enum,extern,fail,false,fn,impl,let,log,loop,match,mod,move,mut,priv,pub,pure,ref,self,static,struct,true,trait,type,unsafe,use"],y=[y,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],R=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)\b/,
+V=/\S/,X=v({keywords:[M,O,E,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",P,Q,y],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),F={};p(X,["default-code"]);p(C([],[["pln",/^[^]+/],["dec",/^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",
+/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^