diff --git a/client/src/components/ActivityPanels/ActivityLevels.less b/client/src/components/ActivityPanels/ActivityLevels.less index cae865e3a..0b2facf48 100644 --- a/client/src/components/ActivityPanels/ActivityLevels.less +++ b/client/src/components/ActivityPanels/ActivityLevels.less @@ -74,6 +74,7 @@ p { top: 100%; } + #icon-control-panel { display: flex; align-items: center; @@ -134,7 +135,7 @@ p { align-items: center; flex: 1; max-height: 8vh; - width: 100%; + width: 1s00%; #logo { max-width: 145px; @@ -194,6 +195,98 @@ p { height: 72vh; } +#newblockly-canvas { + margin: absolute; + width: 55%; + height: 72vh; +} + +.canvas-container { + // display: flex; + // justify-content: space-between; + width: 95%; + margin: auto; + height: 72vh; +} + +#topRight-containter{ + position: absolute; + width: 40%; + height: 30vh; + right: 40px; + top: 60px; +} + +#bottomRight-container{ + position: absolute; + width: 40%; + height: 30vh; + right: 40px; + top: 345px; +} + +#block-bs{ + position: absolute; + width: 40%; + height: 0vh; + right: 40px; + top: 13px; +} + +#pre-text{ + position: absolute; + width: 40%; + height: 0vh; + right: 40px; + top: 60px; +} + +#def-text{ + position: absolute; + width: 40%; + height: 0vh; + right: 40px; + top: 230px; +} + +#gen-text{ + position: absolute; + width: 40%; + height: 0vh; + right: 40px; + top: 400px; +} + +#blocklyCanvasTop { + position: absolute; + width: 40%; + height: 20vh; + right: 40px; + top: 80px; + border: 1px solid #000; /* Add a border, adjust the color and size as needed */ + z-index:1; +} + +#blocklyCanvasMid { + position: absolute; + width: 40%; + height: 20vh; + right: 40px; + top: 250px; + border: 1px solid #000; /* Add a border, adjust the color and size as needed */ + z-index:1; +} + +#blocklyCanvasBottom { + position: absolute; + width: 40%; + height: 20vh; + right: 40px; + top: 420px; + border: 1px solid #000; /* Add a border, adjust the color and size as needed */ + z-index:1; +} + #console-container { background-color: white; border: 1px solid #colors[secondary]; @@ -261,6 +354,61 @@ p { text-align: center; } +#toolbox-background { + // background-color: grey; + // width: 300px; + // border: 15px solid green; + // padding: 50px; + // margin: 20px; + display: flex; + justify-content: center; + align-items: left; + background-color: grey; + //border-radius: 100px; + width: 5%; + //min-height: 200px; + color: #colors[text-secondary]; + font-size: 1.2em; + font-weight: bold; + position: absolute; + top: 80px; + left: 0px; + text-align: center; + overflow: auto; + scroll-behavior: smooth;//might work dunno +} + +//!!!!!!!!!!! +#tb-feature-bg { + display: flex; + justify-content: center; + align-items: left; + //background-color: #colors[secondary]; + //border-radius: 100px; + width: 515%; + //min-height: 200px; + color: #colors[text-secondary]; + position: absolute; + font-weight: bold; + left: 175%; + top: -5px; + text-align: center; +} + +#button-visual { + display: flex; + justify-content: center; + align-items: left; + background-color: grey; + border-radius: 100px; + width: 5%; + min-height: 450px; + color: #colors[text-secondary]; + font-size: 1.2em; + font-weight: bold; + position: absolute; +} + #section-text { font-size: 1em; font-weight: bold; diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/BlocklyCanvasPanel.jsx b/client/src/components/ActivityPanels/BlocklyCanvasPanel/BlocklyCanvasPanel.jsx index 578b64db1..ed76ef410 100644 --- a/client/src/components/ActivityPanels/BlocklyCanvasPanel/BlocklyCanvasPanel.jsx +++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/BlocklyCanvasPanel.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PublicCanvas from './canvas/PublicCanvas'; +import CustomBlock from './canvas/blockFactory/CustomBlock'; import StudentCanvas from './canvas/StudentCanvas'; import MentorCanvas from './canvas/MentorCanvas'; import ContentCreatorCanvas from './canvas/ContentCreatorCanvas'; @@ -10,9 +11,10 @@ const BlocklyCanvasPanel = ({ activity, isSandbox, setActivity }) => { const userRole = value.role; + switch (userRole) { case 'DefaultUser': - return ; + return ; case 'Student': return ; case 'Mentor': diff --git a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/PublicCanvas.jsx b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/PublicCanvas.jsx index cb83c2ff9..17f2665ae 100644 --- a/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/PublicCanvas.jsx +++ b/client/src/components/ActivityPanels/BlocklyCanvasPanel/canvas/PublicCanvas.jsx @@ -13,10 +13,11 @@ import { } from '../../Utils/consoleHelpers'; import ArduinoLogo from '../Icons/ArduinoLogo'; import PlotterLogo from '../Icons/PlotterLogo'; +import CustomBlock from './blockFactory/CustomBlock' let plotId = 1; -export default function PublicCanvas({ activity, isSandbox }) { +export default function PublicCanvas({ activity, isSandbox}) { const [hoverUndo, setHoverUndo] = useState(false); const [hoverRedo, setHoverRedo] = useState(false); const [hoverCompile, setHoverCompile] = useState(false); @@ -32,6 +33,10 @@ export default function PublicCanvas({ activity, isSandbox }) { const workspaceRef = useRef(null); const activityRef = useRef(null); + // useStates for Program your Arduino... / Custom Blocks + const [selectedFeature, setSelectedFeature] = useState('Program your Arduino...'); + const [notSelectedFeature, setNotSelectedFeature] = useState('Custom Blocks') + const setWorkspace = () => { workspaceRef.current = window.Blockly.inject('blockly-canvas', { toolbox: document.getElementById('toolbox'), @@ -154,6 +159,30 @@ export default function PublicCanvas({ activity, isSandbox }) { ); + //Program you Arduino... / Custom Blocks | switch + const featureList = (buttonText, newFeature) => ( + + ); + + if(selectedFeature === 'Custom Blocks'){ + return ; + } return (
@@ -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 = ( + + + +   Show Serial Plotter + + + + + + + ); + + //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
+  
+  
+  
+  
+  
+  
+  
+  
+
+
+  
+    
+      
+      
+    
+    
+      
+      
+    
+  
+

Blockly > + Demos > Block Factory

+
+ + + + + +
+

Preview: + +

+
+ + + +
+
+
+
+
+ + + + + + + + + + + + + + + + +
+
+
+

Language code: + +

+
+

+              
+            
+

Generator stub: + +

+
+

+            
+
+ + + 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",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-", +/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);p(C([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/], +["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);p(C([],[["atv",/^[\S\s]+/]]),["uq.val"]);p(v({keywords:M,hashComments:!0,cStyleComments:!0,types:R}),["c","cc","cpp","cxx","cyc","m"]);p(v({keywords:"null,true,false"}),["json"]);p(v({keywords:O,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:R}), +["cs"]);p(v({keywords:N,cStyleComments:!0}),["java"]);p(v({keywords:y,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);p(v({keywords:P,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);p(v({keywords:"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",hashComments:!0,multiLineStrings:!0,regexLiterals:2}),["perl","pl","pm"]);p(v({keywords:Q, +hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);p(v({keywords:E,cStyleComments:!0,regexLiterals:!0}),["javascript","js"]);p(v({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);p(v({keywords:W,cStyleComments:!0,multilineStrings:!0}),["rc","rs","rust"]); +p(C([],[["str",/^[\S\s]+/]]),["regex"]);var Y=D.PR={createSimpleLexer:C,registerLangHandler:p,sourceDecorator:v,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:D.prettyPrintOne=function(a,d,g){var b=document.createElement("div");b.innerHTML="
"+a+"
";b=b.firstChild;g&&J(b,g,!0);K({h:d,j:g,c:b,i:1}); +return b.innerHTML},prettyPrint:D.prettyPrint=function(a,d){function g(){for(var b=D.PR_SHOULD_USE_CONTINUATION?c.now()+250:Infinity;i