+
+
+ CYF Blocks
+
+
+
+
+
+
diff --git a/client/src/App.js b/client/src/App.js
new file mode 100644
index 0000000..ae8ace5
--- /dev/null
+++ b/client/src/App.js
@@ -0,0 +1,124 @@
+import { useState } from "react";
+import * as Blockly from "blockly/core";
+import locale from "blockly/msg/en";
+import "blockly/blocks";
+
+import "./Blocks/dom";
+import "./Blocks/cyf";
+import useBlockly from "./Blockly/useBlockly";
+
+import * as Exercise1 from "./Exercises/01-stuff";
+import * as Exercise2 from "./Exercises/02-more-stuff";
+
+import Split from "react-split-grid";
+import TextPanel from "./TextPanel/TextPanel";
+import Output from "./Output/Output";
+import Header from "./Layout/Header/Header";
+import Menu from "./Layout/Menu/Menu";
+import Footer from "./Layout/Footer/Footer";
+import Button from "./Button/Button";
+import "./App.scss";
+
+import { ReactComponent as Background } from "../src/svgs/Humaaans-Phone.svg";
+
+Blockly.setLocale(locale);
+
+// just left all this and presumed you will pass whatever you decide to do into the text panel
+const exercises = [Exercise1, Exercise2];
+
+function useExercise() {
+ const [exerciseIndex, setExerciseIndex] = useState(0);
+
+ function nextExercise() {
+ setExerciseIndex(exerciseIndex + 1);
+ }
+ function prevExercise() {
+ setExerciseIndex(exerciseIndex - 1);
+ }
+
+ return {
+ exercise: exercises[exerciseIndex],
+ hasNextExercise: exerciseIndex + 1 < exercises.length,
+ nextExercise,
+ hasPrevExercise: exerciseIndex - 1 >= 0,
+ prevExercise,
+ };
+}
+
+export default function App() {
+ const {
+ exercise,
+ hasNextExercise,
+ nextExercise,
+ hasPrevExercise,
+ prevExercise,
+ } = useExercise();
+
+ const { BlocklyComponent, generate } = useBlockly({
+ toolbox: exercise.toolbox,
+ });
+
+ const [generated, setGenerated] = useState("");
+
+ function handleGenerate() {
+ setGenerated(generate());
+ }
+ // these are resizable panels -- a possible solution to the problem of making all three things available
+ // so the user can compare across text, blocks, and output
+ // not married to this
+
+ return (
+
+
+
+
+ (
+
+
+
+
+
+
+
+ )}
+ />
+
+
+
+ );
+}
diff --git a/client/src/App.scss b/client/src/App.scss
new file mode 100644
index 0000000..ce26725
--- /dev/null
+++ b/client/src/App.scss
@@ -0,0 +1,20 @@
+@import "theme/utilities.scss";
+
+.c-layout {
+ @include grid-assign(header, panels, footer);
+ grid-template-columns: 0 1fr 0;
+ grid-template-areas:
+ ". ...... ."
+ ". header . "
+ ". panels ."
+ ". footer .";
+ gap: var(--theme-spacing--4);
+ place-content: center;
+ max-width: 100vw;
+ overflow-y: hidden;
+
+ &__panels {
+ display: grid;
+ grid-template-columns: 2fr 48px 1fr 48px 6fr;
+ }
+}
diff --git a/client/src/Blockly/Blockly.scss b/client/src/Blockly/Blockly.scss
new file mode 100644
index 0000000..7c6aaf6
--- /dev/null
+++ b/client/src/Blockly/Blockly.scss
@@ -0,0 +1,5 @@
+@import "../theme/utilities.scss";
+
+.c-blockly {
+ height: 100%;
+}
diff --git a/client/src/Blockly/CYFTheme.js b/client/src/Blockly/CYFTheme.js
new file mode 100644
index 0000000..80ae21f
--- /dev/null
+++ b/client/src/Blockly/CYFTheme.js
@@ -0,0 +1,95 @@
+/**
+ * @fileoverview Higher contrast, brandable theme.
+ *
+ */
+
+import Blockly from "blockly/core";
+
+const defaultBlockStyles = {
+ colour_blocks: {
+ colourPrimary: "var(--theme-color--brand)",
+ colourSecondary: "var(--theme-color--accent)",
+ colourTertiary: "var(--theme-color--pop)",
+ },
+ list_blocks: {
+ colourPrimary: "var(--theme-color--brand)",
+ colourSecondary: "#AD7BE9",
+ colourTertiary: "#CDB6E9",
+ },
+ logic_blocks: {
+ colourPrimary: "var(--theme-color--brand)",
+ colourSecondary: "#64C7FF",
+ colourTertiary: "#C5EAFF",
+ },
+ loop_blocks: {
+ colourPrimary: "var(--theme-color--brand)",
+ colourSecondary: "#9AFF78",
+ colourTertiary: "#E1FFD7",
+ },
+ math_blocks: {
+ colourPrimary: "var(--theme-color--brand)",
+ colourSecondary: "#8A9EFF",
+ colourTertiary: "#DCE2FF",
+ },
+ procedure_blocks: {
+ colourPrimary: "var(--theme-color--brand)",
+ colourSecondary: "#77E6EE",
+ colourTertiary: "#CFECEE",
+ },
+ text_blocks: {
+ colourPrimary: "var(--theme-color--brand)",
+ colourSecondary: "#5ae27c",
+ colourTertiary: "#D2FFDD",
+ },
+ variable_blocks: {
+ colourPrimary: "var(--theme-color--brand)",
+ colourSecondary: "#FF73BE",
+ colourTertiary: "#FFD4EB",
+ },
+ variable_dynamic_blocks: {
+ colourPrimary: "var(--theme-color--brand)",
+ colourSecondary: "#FF73BE",
+ colourTertiary: "#FFD4EB",
+ },
+ hat_blocks: {
+ colourPrimary: "#880e4f",
+ colourSecondary: "#FF73BE",
+ colourTertiary: "#FFD4EB",
+ hat: "cap",
+ },
+};
+
+const categoryStyles = {
+ colour_category: { colour: "var(--theme-color--brand)" },
+ list_category: { colour: "var(--theme-color--brand)" },
+ logic_category: { colour: "var(--theme-color--brand)" },
+ loop_category: { colour: "var(--theme-color--brand)" },
+ math_category: { colour: "var(--theme-color--brand)" },
+ procedure_category: { colour: "var(--theme-color--brand)" },
+ text_category: { colour: "var(--theme-color--brand)" },
+ variable_category: { colour: "var(--theme-color--brand)" },
+ variable_dynamic_category: { colour: "var(--theme-color--brand)" },
+};
+
+// Temporarily required to ensure there's no conflict with
+// Blockly.Themes.HighContrast
+// Blockly.registry.unregister("theme", "highcontrast");
+
+/**
+ * High contrast theme.
+ */
+export default Blockly.Theme.defineTheme("highcontrast", {
+ blockStyles: defaultBlockStyles,
+ categoryStyles: categoryStyles,
+ componentStyles: {
+ selectedGlowColour: "#000000",
+ selectedGlowSize: 1,
+ replacementGlowColour: "#000000",
+ },
+ fontStyle: {
+ family: "var(--theme-font-display)", // Use default font-family.
+ weight: null, // Use default font-weight.
+ size: 16,
+ },
+ startHats: null,
+});
diff --git a/client/src/Blockly/useBlockly.js b/client/src/Blockly/useBlockly.js
new file mode 100644
index 0000000..b06466e
--- /dev/null
+++ b/client/src/Blockly/useBlockly.js
@@ -0,0 +1,56 @@
+import { useMemo, useRef } from "react";
+import useDeepCompareEffect from "use-deep-compare-effect";
+import _ from "lodash";
+import * as Blockly from "blockly/core";
+import BlocklyJS from "blockly/javascript";
+import "./Blockly.scss";
+import * as Theme from "./CYFTheme";
+
+export default function useBlockly({ initialBlock, toolbox, theme }) {
+ const wrapperRef = useRef();
+ const workspaceRef = useRef();
+
+ // Since the deps are objects, we need to deep compare them
+ useDeepCompareEffect(() => {
+ // Inject the workspace
+ workspaceRef.current = Blockly.inject(wrapperRef.current, {
+ // Unfortuntely Blockly mutates the toolbox object when initialising. This
+ // means that the dep changes between renders, which in turn means that
+ // the workspace is re-injected
+ toolbox: _.cloneDeep(toolbox),
+ theme: Theme,
+ // does this mean it cannot be https://developers.google.com/blockly/guides/configure/web/resizable ?
+ });
+
+ // Set the initial block in the workspace
+ if (initialBlock) {
+ let block = workspaceRef.current.newBlock(initialBlock.kind);
+ block.moveBy(initialBlock.x, initialBlock.y);
+ block.initSvg();
+
+ workspaceRef.current.render();
+ }
+
+ return () => {
+ workspaceRef.current.dispose();
+ };
+ }, [toolbox, initialBlock, theme]);
+
+ return useMemo(
+ () => ({
+ // Return a component to inject the workspace
+ BlocklyComponent: () => (
+
+ ),
+ // Generate code from the workspace
+ generate: () => {
+ return BlocklyJS.workspaceToCode(workspaceRef.current);
+ },
+ }),
+ []
+ );
+}
diff --git a/client/src/Blocks/cyf.js b/client/src/Blocks/cyf.js
new file mode 100644
index 0000000..e38dab7
--- /dev/null
+++ b/client/src/Blocks/cyf.js
@@ -0,0 +1,145 @@
+import * as Blockly from "blockly/core";
+import BlocklyJavaScript from "blockly/javascript";
+
+function provideRandomInt() {
+ return BlocklyJavaScript.provideFunction_("randomInt", [
+ "function " + BlocklyJavaScript.FUNCTION_NAME_PLACEHOLDER_ + "(n) {",
+ " // Return a random number from in [0, n[",
+ " return Math.floor(Math.random()*n);",
+ "}",
+ ]);
+}
+
+function provideRandomMember() {
+ let randomInt = provideRandomInt();
+ return BlocklyJavaScript.provideFunction_("randomMember", [
+ "function " + BlocklyJavaScript.FUNCTION_NAME_PLACEHOLDER_ + "(arr) {",
+ " // Return a random member of the array",
+ " return arr[" + randomInt + "(arr.length)]",
+ "}",
+ ]);
+}
+
+Blockly.defineBlocksWithJsonArray([
+ {
+ type: "get_randomWord",
+ message0: "get a random %1",
+ args0: [
+ {
+ type: "field_dropdown",
+ name: "TYPE",
+ options: [
+ ["word", "WORD"],
+ ["noun", "NOUN"],
+ ["verb", "VERB"],
+ ["adjective", "ADJECTIVE"],
+ ],
+ },
+ ],
+ output: "String",
+ colour: "%{BKY_TEXTS_HUE}",
+ },
+ {
+ type: "get_randomMember",
+ message0: "get a random item from array %1",
+ args0: [
+ {
+ type: "input_value",
+ name: "ARRAY",
+ },
+ ],
+ output: null,
+ style: "list_blocks",
+ },
+ {
+ type: "text_to_speech",
+ message0: "say %1",
+ args0: [
+ {
+ type: "input_value",
+ name: "VALUE",
+ },
+ ],
+ previousStatement: null,
+ nextStatement: null,
+ colour: "%{BKY_TEXTS_HUE}",
+ },
+ {
+ type: "get_sum",
+ message0: "get the sum of the numbers in array %1",
+ args0: [
+ {
+ type: "input_value",
+ name: "ARRAY",
+ },
+ ],
+ output: Number,
+ style: "list_blocks",
+ },
+]);
+
+BlocklyJavaScript["get_randomWord"] = function (block) {
+ const getWords = BlocklyJavaScript.provideFunction_("getWords", [
+ "function " + BlocklyJavaScript.FUNCTION_NAME_PLACEHOLDER_ + "(type) {",
+ " // Return words of a given type, or all words if type is 'WORD'",
+ " let words = [",
+ " {type: 'ADJECTIVE', value: 'big'},",
+ " {type: 'ADJECTIVE', value: 'purple'},",
+ " {type: 'ADJECTIVE', value: 'miscellaneous'},",
+ " {type: 'ADJECTIVE', value: 'interesting'},",
+ " {type: 'ADJECTIVE', value: 'collapsed'},",
+ " {type: 'NOUN', value: 'umbrella'},",
+ " {type: 'NOUN', value: 'knee'},",
+ " {type: 'NOUN', value: 'banana'},",
+ " {type: 'NOUN', value: 'platypus'},",
+ " {type: 'NOUN', value: 'bottle'},",
+ " {type: 'VERB', value: 'delineated'},",
+ " {type: 'VERB', value: 'read'},",
+ " {type: 'VERB', value: 'saw'},",
+ " {type: 'VERB', value: 'ate'},",
+ " {type: 'VERB', value: 'magicked'},",
+ " ];",
+ " return words.filter(word => type === 'WORD' || word.type === type).map(word => word.value);",
+ "}",
+ ]);
+ const type = block.getFieldValue("TYPE");
+ const randomMember = provideRandomMember();
+ return [
+ `${randomMember}(${getWords}('${type}'))`,
+ BlocklyJavaScript.ORDER_FUNCTION_CALL,
+ ];
+};
+
+BlocklyJavaScript["get_randomMember"] = function (block) {
+ const randomMember = provideRandomMember();
+ var array =
+ BlocklyJavaScript.valueToCode(
+ block,
+ "ARRAY",
+ BlocklyJavaScript.ORDER_NONE
+ ) || "[]";
+ return [`${randomMember}(${array})`, BlocklyJavaScript.ORDER_FUNCTION_CALL];
+};
+
+BlocklyJavaScript["get_sum"] = function (block) {
+ var array =
+ BlocklyJavaScript.valueToCode(
+ block,
+ "ARRAY",
+ BlocklyJavaScript.ORDER_MEMBER
+ ) || "[]";
+ return [
+ `${array}.reduce((a,b) => a+b, 0)`,
+ BlocklyJavaScript.ORDER_FUNCTION_CALL,
+ ];
+};
+
+BlocklyJavaScript["text_to_speech"] = function (block) {
+ var utterance =
+ BlocklyJavaScript.valueToCode(
+ block,
+ "VALUE",
+ BlocklyJavaScript.ORDER_NONE
+ ) || "";
+ return `window.speechSynthesis.speak(new SpeechSynthesisUtterance(${utterance}));\n`;
+};
diff --git a/client/src/Blocks/dom.js b/client/src/Blocks/dom.js
new file mode 100644
index 0000000..8316254
--- /dev/null
+++ b/client/src/Blocks/dom.js
@@ -0,0 +1,691 @@
+import * as Blockly from "blockly/core";
+import BlocklyJavaScript from "blockly/javascript";
+
+// customised messages
+Blockly.Msg["CONTROLS_FOREACH_TITLE"] = "for each item %1 in array %2";
+
+Blockly.Msg["LISTS_CREATE_EMPTY_TITLE"] = "create empty array";
+Blockly.Msg["LISTS_CREATE_EMPTY_TOOLTIP"] =
+ "Returns an array, of length 0, containing no data records";
+Blockly.Msg["LISTS_CREATE_WITH_CONTAINER_TITLE_ADD"] = "array";
+Blockly.Msg["LISTS_CREATE_WITH_CONTAINER_TOOLTIP"] =
+ "Add, remove, or reorder sections to reconfigure this array block.";
+Blockly.Msg["LISTS_CREATE_WITH_INPUT_WITH"] = "create array with";
+Blockly.Msg["LISTS_CREATE_WITH_ITEM_TOOLTIP"] = "Add an item to the array.";
+Blockly.Msg["LISTS_CREATE_WITH_TOOLTIP"] =
+ "Create an array with any number of items.";
+
+// copied from Blockly.Generator.prototype.statementToCode (but doesn't add indentation)
+BlocklyJavaScript.statementToCodeNoIndent = function (block, name) {
+ var targetBlock = block.getInputTargetBlock(name);
+ var code = this.blockToCode(targetBlock);
+ // Value blocks must return code and order of operations info.
+ // Statement blocks must only return code.
+ if (typeof code !== "string") {
+ throw TypeError(
+ "Expecting code from statement block: " +
+ (targetBlock && targetBlock.type)
+ );
+ }
+ return code;
+};
+
+BlocklyJavaScript.getWithContextVariable = function () {
+ if (!(this.contextVariableStack && this.contextVariableStack[0])) {
+ return "";
+ }
+ return this.contextVariableStack[0];
+};
+
+BlocklyJavaScript.pushWithContextVariable = function (variableName) {
+ this.contextVariableStack = this.contextVariableStack || [];
+ this.contextVariableStack.unshift(variableName);
+};
+
+BlocklyJavaScript.popWithContextVariable = function () {
+ if (!(this.contextVariableStack && this.contextVariableStack[0])) {
+ return "";
+ }
+ return this.contextVariableStack.shift();
+};
+
+Blockly.Blocks["arrays_getFirst"] = {
+ /**
+ * 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"],
+ ];
+ var WHERE_OPTIONS = [
+ [Blockly.Msg["LISTS_GET_INDEX_FIRST"], "FIRST"],
+ [Blockly.Msg["LISTS_GET_INDEX_LAST"], "LAST"],
+ ];
+ this.setStyle("list_blocks");
+
+ var modeMenu = new Blockly.FieldDropdown(MODE, function (value) {
+ var isStatement = value === "REMOVE";
+ this.getSourceBlock().updateStatement_(isStatement);
+ });
+ this.appendDummyInput()
+ .appendField("", "SPACE")
+ .appendField(modeMenu, "MODE");
+
+ var atMenu = new Blockly.FieldDropdown(WHERE_OPTIONS);
+ this.appendDummyInput().appendField("the").appendField(atMenu, "WHERE");
+ this.appendValueInput("VALUE")
+ .setCheck("Array")
+ .appendField("item from the array");
+
+ this.setInputsInline(true);
+ this.setOutput(true);
+ },
+ /*
+ * mutationToDom and domToMutation are only here for backward compatibilty with xml (probably never needed)
+ */
+ /**
+ * 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 = Blockly.utils.xml.createElement("mutation");
+ var isStatement = !this.outputConnection;
+ container.setAttribute("statement", isStatement);
+ 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);
+ },
+ /**
+ * 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);
+ }
+ }
+ },
+};
+
+const WITH_CONTEXTS = [
+ "add_element",
+ "with_element_by_id",
+ "with_element_by_selector",
+ "with_elements_by_selector",
+ "element_clicked",
+];
+
+function validateInWithContext(_e) {
+ if (!this.workspace.isDragging || this.workspace.isDragging()) {
+ return; // Don't change state at the start of a drag.
+ }
+ var legal = false;
+ // Is the block nested in a procedure?
+ var block = this.getSurroundParent();
+ while (block) {
+ if (WITH_CONTEXTS.indexOf(block.type) !== -1) {
+ legal = true;
+ break;
+ }
+ block = block.getSurroundParent();
+ }
+ if (legal) {
+ this.setWarningText(null);
+ if (!this.isInFlyout) {
+ this.setEnabled(true);
+ }
+ } else {
+ this.setWarningText(
+ "This block can only be used inside the 'create a new ... element' and 'find the element with id' blocks"
+ );
+ if (!this.isInFlyout && !this.getInheritedDisabled()) {
+ this.setEnabled(false);
+ }
+ }
+}
+
+Blockly.Extensions.register("validate_in_with_context", function () {
+ this.setOnChange(validateInWithContext);
+});
+
+Blockly.defineBlocksWithJsonArray([
+ {
+ type: "element_clicked",
+ message0: "%1 %2 %3 %4 %5",
+ args0: [
+ {
+ type: "field_label_serializable",
+ name: "TEXT1",
+ text: "when the element with id",
+ },
+ {
+ type: "field_input",
+ name: "ID",
+ text: "button",
+ },
+ {
+ type: "field_label_serializable",
+ name: "TEXT2",
+ text: "is clicked",
+ },
+ {
+ type: "input_dummy",
+ },
+ {
+ type: "input_statement",
+ name: "HANDLER",
+ },
+ ],
+ colour: "%{BKY_COLOUR_HUE}",
+ tooltip: "When a button is clicked",
+ helpUrl: "",
+ },
+ {
+ type: "element_clicked_current",
+ message0: "%1 %2 %3",
+ args0: [
+ {
+ type: "field_label_serializable",
+ name: "TEXT1",
+ text: "when the element is clicked",
+ },
+ {
+ type: "input_dummy",
+ },
+ {
+ type: "input_statement",
+ name: "HANDLER",
+ },
+ ],
+ previousStatement: null,
+ nextStatement: null,
+ colour: 60,
+ tooltip: "When a button is clicked",
+ helpUrl: "",
+ extensions: ["validate_in_with_context"],
+ },
+ {
+ type: "on_start",
+ message0: "%1 %2 %3",
+ args0: [
+ {
+ type: "field_label_serializable",
+ name: "TEXT1",
+ text: "at the start (when run is clicked)",
+ },
+ {
+ type: "input_dummy",
+ },
+ {
+ type: "input_statement",
+ name: "HANDLER",
+ },
+ ],
+ inputsInline: false,
+ colour: "%{BKY_COLOUR_HUE}",
+ tooltip: "At the start",
+ helpUrl: "",
+ },
+ {
+ type: "set_attribute",
+ message0: "set the attribute %1 to %2",
+ args0: [
+ {
+ type: "field_dropdown",
+ name: "PROPERTY",
+ options: [
+ ["src", "src"],
+ ["href", "href"],
+ ["background", "backgroundColor"],
+ ["color", "color"],
+ ["id", "id"],
+ ["class", "class"],
+ ["is visible", "visibility"],
+ ],
+ },
+ {
+ type: "input_value",
+ name: "VALUE",
+ },
+ ],
+ previousStatement: null,
+ nextStatement: null,
+ colour: 60,
+ extensions: ["validate_in_with_context"],
+ },
+ {
+ type: "set_content",
+ message0: "set the text content to %1",
+ args0: [
+ {
+ type: "input_value",
+ name: "VALUE",
+ },
+ ],
+ previousStatement: null,
+ nextStatement: null,
+ colour: 60,
+ extensions: ["validate_in_with_context"],
+ },
+ {
+ type: "get_input_value_with_id",
+ message0: "get the value of the with id %1",
+ args0: [
+ {
+ type: "field_input",
+ name: "ID",
+ text: "text",
+ },
+ ],
+ output: "String",
+ colour: 60,
+ },
+ {
+ type: "remove_contents",
+ message0: "remove the contents of the element",
+ args0: [],
+ previousStatement: null,
+ nextStatement: null,
+ colour: 60,
+ extensions: ["validate_in_with_context"],
+ },
+ {
+ type: "arrays_push",
+ message0: "add %1 at the %2 of array %3",
+ args0: [
+ {
+ type: "input_value",
+ name: "VALUE",
+ },
+ {
+ type: "field_dropdown",
+ name: "WHERE",
+ options: [
+ ["start", "START"],
+ ["end", "END"],
+ ],
+ },
+ {
+ type: "input_value",
+ name: "LIST",
+ check: "Array",
+ },
+ ],
+ previousStatement: null,
+ nextStatement: null,
+ style: "list_blocks",
+ },
+ {
+ type: "arrays_forEach",
+ message0: "%{BKY_CONTROLS_FOREACH_TITLE}",
+ args0: [
+ {
+ type: "field_variable",
+ name: "VAR",
+ variable: null,
+ },
+ {
+ type: "input_value",
+ name: "LIST",
+ check: "Array",
+ },
+ ],
+ message1: "%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",
+ args1: [
+ {
+ type: "input_statement",
+ name: "DO",
+ },
+ ],
+ previousStatement: null,
+ nextStatement: null,
+ style: "loop_blocks",
+ extensions: ["contextMenu_newGetVariableBlock", "controls_forEach_tooltip"],
+ },
+]);
+
+BlocklyJavaScript["arrays_getFirst"] = function (block) {
+ var mode = block.getFieldValue("MODE");
+ var where = block.getFieldValue("WHERE");
+ var listOrder = BlocklyJavaScript.ORDER_MEMBER;
+ var list = BlocklyJavaScript.valueToCode(block, "VALUE", listOrder) || "[]";
+ if (mode === "GET") {
+ let op = where === "FIRST" ? "[0]" : ".slice(-1)[0]";
+ let code = list + op;
+ return [code, BlocklyJavaScript.ORDER_MEMBER];
+ } else if (mode === "GET_REMOVE") {
+ let op = where === "FIRST" ? ".shift()" : ".pop()";
+ let code = list + op;
+ return [code, BlocklyJavaScript.ORDER_MEMBER];
+ } else if (mode === "REMOVE") {
+ let op = where === "FIRST" ? ".shift()" : ".pop()";
+ return list + op + ";\n";
+ }
+};
+
+BlocklyJavaScript["arrays_push"] = function (block) {
+ var where = block.getFieldValue("WHERE");
+ var list =
+ BlocklyJavaScript.valueToCode(
+ block,
+ "LIST",
+ BlocklyJavaScript.ORDER_MEMBER
+ ) || "[]";
+ var value =
+ BlocklyJavaScript.valueToCode(
+ block,
+ "VALUE",
+ BlocklyJavaScript.ORDER_NONE // going to be passed as a single argument to unshift
+ ) || "null";
+ var op = where === "START" ? ".unshift" : ".push";
+ return list + op + "(" + value + ");\n";
+};
+
+BlocklyJavaScript["arrays_forEach"] = function (block) {
+ var variable = BlocklyJavaScript.nameDB_.getName(
+ block.getFieldValue("VAR"),
+ Blockly.VARIABLE_CATEGORY_NAME
+ );
+ var list =
+ BlocklyJavaScript.valueToCode(
+ block,
+ "LIST",
+ BlocklyJavaScript.ORDER_MEMBER
+ ) || "[]";
+ var branch = BlocklyJavaScript.statementToCode(block, "DO");
+ // don't need a loop trap
+ var code = "";
+ code += list + ".forEach((" + variable + ") => {\n" + branch + "});\n";
+ return code;
+};
+
+BlocklyJavaScript["element_clicked"] = function (block) {
+ var text_id = block.getFieldValue("ID");
+
+ //let branch = BlocklyJavaScript.statementToCodeNoIndent(block, "STACK");
+ BlocklyJavaScript.pushWithContextVariable("event.target");
+ var statements_handler = BlocklyJavaScript.statementToCode(block, "HANDLER");
+ BlocklyJavaScript.popWithContextVariable();
+ // TODO: Assemble JavaScript into code variable.
+ var code = `
+document.getElementById('${text_id}').addEventListener('click', (event) => {
+${statements_handler}
+});`;
+ return code;
+};
+
+/* Find the variables referenced inside the block. Modify the nameDB/variableMap so that
+ * they think they are called name_.
+ * returns an array of object.
+ * The globalName is the name of the global variable
+ * The localName is the new local name that will be used until restoreGlobalVariables is called.
+ */
+function renameLocalVariablesUsedInBlock(block) {
+ let usedVariablesById = {};
+ block
+ .getDescendants()
+ .filter((b) => b.type === "variables_get")
+ .forEach((b) => {
+ usedVariablesById[b.getFieldValue("VAR")] = {
+ id: b.getFieldValue("VAR"),
+ };
+ });
+ let renamedVariables = Object.values(usedVariablesById);
+ renamedVariables.forEach((variable) => {
+ variable.blocklyVariable =
+ BlocklyJavaScript.nameDB_.variableMap_.getVariableById(variable.id);
+ variable.globalName = variable.blocklyVariable.name;
+ variable.blocklyVariable.name = variable.globalName + "_local";
+ // ensure no collisions
+ variable.localName = BlocklyJavaScript.nameDB_.getName(
+ variable.id,
+ Blockly.Variables.NAME_TYPE
+ );
+ });
+ return renamedVariables;
+}
+
+function restoreGlobalVariables(localVariables) {
+ localVariables.forEach((variable) => {
+ variable.blocklyVariable.name = variable.globalName;
+ });
+}
+
+BlocklyJavaScript["element_clicked_current"] = function (block) {
+ let localVariables = renameLocalVariablesUsedInBlock(block);
+ var statements_handler = BlocklyJavaScript.statementToCode(block, "HANDLER");
+ restoreGlobalVariables(localVariables);
+
+ var code = "\n";
+ localVariables.forEach((variable) => {
+ code += `// deep copy global variable '${variable.globalName}' so the current
+// value is available inside the event listener
+let ${variable.localName} = JSON.parse(JSON.stringify(${variable.globalName}));
+`;
+ });
+ let withContextVariable = BlocklyJavaScript.getWithContextVariable();
+ code += `${withContextVariable}.addEventListener('click', (event) => {
+${statements_handler}
+});`;
+ return code;
+};
+
+BlocklyJavaScript["on_start"] = function (block) {
+ var code = BlocklyJavaScript.statementToCodeNoIndent(block, "HANDLER");
+ return code;
+};
+
+Blockly.Blocks["with_element_by_id"] = {
+ init: function () {
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setColour(45);
+ this.appendDummyInput()
+ .appendField("find the element with id")
+ .appendField(new Blockly.FieldTextInput("list"), "ID");
+ this.appendStatementInput("STACK").appendField("and");
+ },
+};
+
+BlocklyJavaScript["with_element_by_id"] = function (block) {
+ let elementId = block.getFieldValue("ID");
+ let elementVar = BlocklyJavaScript.nameDB_.getDistinctName(
+ "element_" + elementId,
+ Blockly.Variables.NAME_TYPE
+ );
+ BlocklyJavaScript.pushWithContextVariable(elementVar);
+ let branch = BlocklyJavaScript.statementToCodeNoIndent(block, "STACK");
+ BlocklyJavaScript.popWithContextVariable();
+ return `let ${elementVar} = document.getElementById(${BlocklyJavaScript.quote_(
+ elementId
+ )});
+${branch}`;
+};
+
+Blockly.Blocks["with_element_by_selector"] = {
+ init: function () {
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setColour(45);
+ this.appendDummyInput()
+ .appendField("find the element using css selector")
+ .appendField(new Blockly.FieldTextInput("#list"), "QUERY");
+ this.appendStatementInput("STACK").appendField("and");
+ },
+};
+
+BlocklyJavaScript["with_element_by_selector"] = function (block) {
+ let elementQuery = block.getFieldValue("QUERY");
+ let elementVar = BlocklyJavaScript.nameDB_.getDistinctName(
+ "selectedElement",
+ Blockly.Variables.NAME_TYPE
+ );
+ BlocklyJavaScript.pushWithContextVariable(elementVar);
+ let branch = BlocklyJavaScript.statementToCodeNoIndent(block, "STACK");
+ BlocklyJavaScript.popWithContextVariable();
+ return `let ${elementVar} = document.querySelector(${BlocklyJavaScript.quote_(
+ elementQuery
+ )});
+${branch}`;
+};
+
+Blockly.Blocks["with_elements_by_selector"] = {
+ init: function () {
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setColour(45);
+ this.appendDummyInput()
+ .appendField("find all the elements using css selector")
+ .appendField(new Blockly.FieldTextInput("#list"), "QUERY");
+ this.appendStatementInput("STACK").appendField("and with each");
+ },
+};
+
+BlocklyJavaScript["with_elements_by_selector"] = function (block) {
+ let elementQuery = block.getFieldValue("QUERY");
+ let elementVar = BlocklyJavaScript.nameDB_.getDistinctName(
+ "selectedElement",
+ Blockly.Variables.NAME_TYPE
+ );
+ BlocklyJavaScript.pushWithContextVariable(elementVar);
+ let branch = BlocklyJavaScript.statementToCode(block, "STACK");
+ BlocklyJavaScript.popWithContextVariable();
+ return `document.querySelectorAll(${BlocklyJavaScript.quote_(
+ elementQuery
+ )}).forEach((${elementVar}) => {
+${branch}
+});
+`;
+};
+
+BlocklyJavaScript["set_attribute"] = function (block) {
+ let value = BlocklyJavaScript.valueToCode(
+ block,
+ "VALUE",
+ BlocklyJavaScript.ORDER_ASSIGNMENT
+ );
+ let property = block.getFieldValue("PROPERTY");
+ if (property === "visibility") {
+ value = `(${value}) ? 'visible' : 'hidden'`;
+ }
+ let styleAttributes = ["color", "backgroundColor", "visibility"];
+ let withContextVariable = BlocklyJavaScript.getWithContextVariable();
+ if (styleAttributes.includes(property)) {
+ return withContextVariable + ".style." + property + " = " + value + ";\n";
+ } else {
+ return (
+ withContextVariable +
+ '.setAttribute("' +
+ property +
+ '", ' +
+ value +
+ ");\n"
+ );
+ }
+};
+
+BlocklyJavaScript["set_content"] = function (block) {
+ let value = BlocklyJavaScript.valueToCode(
+ block,
+ "VALUE",
+ BlocklyJavaScript.ORDER_ASSIGNMENT
+ );
+ let withContextVariable = BlocklyJavaScript.getWithContextVariable();
+ return withContextVariable + ".innerText = " + value + ";\n";
+};
+
+BlocklyJavaScript["get_input_value_with_id"] = function (block) {
+ let elementId = block.getFieldValue("ID");
+ BlocklyJavaScript.provideFunction_("getNumberOrString", [
+ "function " + BlocklyJavaScript.FUNCTION_NAME_PLACEHOLDER_ + "(value) {",
+ " // Convert a string value to a number if possible",
+ " let number_value = Number(value);",
+ " if (Number.isNaN(number_value)) {",
+ " return value",
+ " } else {",
+ " return number_value",
+ " }",
+ "}",
+ ]);
+ return [
+ `getNumberOrString(document.getElementById(${BlocklyJavaScript.quote_(
+ elementId
+ )}).value)`,
+ BlocklyJavaScript.ORDER_MEMBER,
+ ];
+};
+
+BlocklyJavaScript["remove_contents"] = function (block) {
+ let withContextVariable = BlocklyJavaScript.getWithContextVariable();
+ return withContextVariable + ".replaceChildren();\n";
+};
+
+Blockly.Blocks["add_element"] = {
+ init: function () {
+ this.setPreviousStatement(true);
+ this.setNextStatement(true);
+ this.setColour(60);
+ this.appendDummyInput()
+ .appendField("create a new")
+ .appendField(
+ new Blockly.FieldDropdown([
+ ["