Skip to content

Commit

Permalink
Started migration of Blockly internals to typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
hrajchert committed May 9, 2023
1 parent e8dec92 commit c7446fc
Show file tree
Hide file tree
Showing 2 changed files with 401 additions and 350 deletions.
386 changes: 36 additions & 350 deletions marlowe-playground-client/src/Blockly/Internal.js
Original file line number Diff line number Diff line change
@@ -1,350 +1,36 @@
import jsonBigInt from "json-bigint";
import { registerDateTimeField } from "src/Blockly/DateTimeField.js";
const JSONbig = jsonBigInt({ useNativeBigInt: true });

export const createBlocklyInstance_ = () => {
return import("blockly");
};

export const debugBlockly = (name) => (state) => () => {
if (typeof window.blockly === "undefined") {
window.blockly = {};
}
window.blockly[name] = state;
};

export const createWorkspace =
(blockly) => (workspaceDiv) => (config) => (tzInfo) => () => {
/* Disable comments */
try {
blockly.ContextMenuRegistry.registry.unregister("blockComment");
} catch (err) {}

/* Disable disabling blocks */
try {
blockly.ContextMenuRegistry.registry.unregister("blockDisable");
} catch (err) {}

/* Register extensions */
/* Silently clean if already registered */
try {
blockly.Extensions.register("hash_validator", function () {});
} catch (err) {}
blockly.Extensions.unregister("hash_validator");
try {
blockly.Extensions.register("number_validator", function () {});
} catch (err) {}
blockly.Extensions.unregister("number_validator");
try {
blockly.Extensions.register("dynamic_timeout_type", function () {});
} catch (err) {}
blockly.Extensions.unregister("dynamic_timeout_type");

/* Hash extension (advanced validation for the hash fields) */
blockly.Extensions.register("hash_validator", function () {
var thisBlock = this;

/* Validator for hash */
var hashValidator = function (input) {
var cleanedInput = input
.replace(new RegExp("[^a-fA-F0-9]+", "g"), "")
.toLowerCase();
if (new RegExp("^([a-f0-9][a-f0-9])*$", "g").test(cleanedInput)) {
return cleanedInput;
} else {
return null;
}
};

["currency_symbol"].forEach(function (fieldName) {
var field = thisBlock.getField(fieldName);
if (field != null) {
field.setValidator(hashValidator);
}
});
});

/* Number extension (advanced validation for number fields - other than timeout) */
blockly.Extensions.register("number_validator", function () {
var thisBlock = this;

/* Validator for number fields */
var numberValidator = function (input) {
if (!isFinite(input)) {
return null;
}
};

thisBlock.inputList.forEach((input) => {
input.fieldRow.forEach((field) => {
if (field instanceof blockly.FieldNumber) {
field.setValidator(numberValidator);
}
});
});
});

const FieldDateTime = registerDateTimeField(blockly);

// This extension takes care of changing the `timeout field` depending on the value of
// `timeout_type`. When `timeout_type` is a constant, then we show a datetime picker
// if it is a parameter we show a text field.
blockly.Extensions.register("dynamic_timeout_type", function () {
const timeoutTypeField = this.getField("timeout_type");
// The timeoutField is mutable as we change it depending of the value of
// the timeout type.
let timeoutField = this.getField("timeout");
// The field Row is what groups a line in the block. In the case of a when block
// this is ["After" label, timeoutTypeField, timeoutField]
const row = timeoutField.getParentInput();
const safeRemoveField = function (fieldName) {
if (row.fieldRow.findIndex((field) => field.name === fieldName) > -1) {
row.removeField(fieldName);
}
};
// We store in this mutable data the values of the timeout field indexed by the different
// timeout types. We initialize this as undefined as there is no blockly event to get the initial
// loaded data, so we mark this information to be gathered on a different way.
let fieldValues = undefined; // { time :: String | undefined, time_param :: String };

// The onChange function lets you know about Blockly events of the entire workspace, visual
// changes, data changes, etc.
const thisBlock = this;
this.setOnChange(function (event) {
// we only care about events for this block.
if (event.blockId != thisBlock.id) return;

timeoutField = thisBlock.getField("timeout");

// This function sets the Timeout Field of the correct type
const updateTimeoutField = function (type) {
if (type == "time") {
safeRemoveField("timeout");
row.appendField(
new FieldDateTime(fieldValues["time"], undefined, tzInfo),
"timeout"
);
} else if (type == "time_param") {
safeRemoveField("timeout");
row.appendField(
new blockly.FieldTextInput(fieldValues["time_param"]),
"timeout"
);
// For some reason Blockly doens't automatically fire this event
// indicating that the timeout field has changed. Not firing the
// event results in a bug where if you attach a new When block,
// change to time_param and convert to marlowe, the old time value
// is presented.
blockly.Events.fire(
new blockly.Events.Change(
thisBlock, // block that changed
"field", // type of element that changed
"timeout", // name of the element that changed
fieldValues["time"], // old value
fieldValues["time_param"] // new value
)
);
}
};

// For the first event we receive, we set the fieldValues to whatever is stored in
// the timeoutField.
if (typeof fieldValues === "undefined") {
const type = timeoutTypeField.getValue();
const val = timeoutField.getValue();

fieldValues = {
// If the timeout type was set to constant, then set the value here and a sensible
// default for time_param
time: type == "time" ? val : undefined,
// If the timeout type was set to a time parameter, then set the value here and
// use undefined for `time`. That will result than on the first switch to a Constant, the
// current time will be used.
time_param: type == "time_param" ? val : "time_param",
};
// Set the timeout field to the correct type
updateTimeoutField(type);
}

if (event.element == "field" && event.name == "timeout") {
// If the timeout field changes, update the fieldValues "local store"
fieldValues[timeoutTypeField.getValue()] = event.newValue;
} else if (event.element == "field" && event.name == "timeout_type") {
// If the timeout_type field changes, then update the timeout field
updateTimeoutField(event.newValue);
}
});
});

/* Inject workspace */
var workspace = blockly.inject(workspaceDiv, config);
blockly.svgResize(workspace);

return workspace;
};

export const resize = (blockly) => (workspace) => () => {
blockly.svgResize(workspace);
workspace.render();
};

function removeUndefinedFields(obj) {
for (var propName in obj) {
if (obj[propName] === undefined) {
delete obj[propName];
}
}
}

function removeEmptyArrayFields(obj) {
for (var propName in obj) {
if (Array.isArray(obj[propName]) && obj[propName].length == 0) {
delete obj[propName];
}
}
}

export const addBlockType_ = (blockly) => (name) => (block) => () => {
// we really don't want to be mutating the input object, it is not supposed to be state
var clone = JSONbig.parse(JSONbig.stringify(block));
removeUndefinedFields(clone);
removeEmptyArrayFields(clone);
blockly.Blocks[name] = {
init: function () {
this.jsonInit(clone);
},
};
};

export const initializeWorkspace_ =
(blockly) => (workspace) => (workspaceBlocks) => () => {
blockly.Xml.domToWorkspace(workspaceBlocks, workspace);
workspace.getAllBlocks()[0].setDeletable(false);
};

export const render = (workspace) => () => {
workspace.render();
};

export const getBlockById_ =
(just) => (nothing) => (workspace) => (id) => () => {
var result = workspace.getBlockById(id);
if (result) {
return just(result);
} else {
return nothing;
}
};

export const workspaceXML = (blockly) => (workspace) => () => {
const isEmpty = workspace.getAllBlocks()[0].getChildren().length == 0;
if (isEmpty) {
return "";
} else {
var dom = blockly.Xml.workspaceToDom(workspace);
return blockly.utils.xml.domToText(dom);
}
};

export const loadWorkspace = (blockly) => (workspace) => (xml) => () => {
var dom = blockly.utils.xml.textToDomDocument(xml);
blockly.Xml.clearWorkspaceAndLoadFromXml(dom.childNodes[0], workspace);
workspace.getAllBlocks()[0].setDeletable(false);
};

export const addChangeListener = (workspace) => (listener) => () => {
workspace.addChangeListener(listener);
};

export const removeChangeListener = (workspace) => (listener) => () => {
workspace.removeChangeListener(listener);
};

export const workspaceToDom = (blockly) => (workspace) => () => {
return blockly.Xml.workspaceToDom(workspace);
};

export const select = (block) => () => {
block.select();
};

export const centerOnBlock = (workspace) => (blockId) => () => {
workspace.centerOnBlock(blockId);
};

export const hideChaff = (blockly) => () => {
blockly.hideChaff();
};

export const getBlockType = (block) => {
return block.type;
};

export const updateToolbox_ = (toolboxJson) => (workspace) => () => {
workspace.updateToolbox(toolboxJson);
};

export const clearUndoStack = (workspace) => () => {
workspace.clearUndo();
};

export const isWorkspaceEmpty = (workspace) => () => {
var topBlocks = workspace.getTopBlocks(false);
return topBlocks == null || topBlocks.length == 0;
};

export const setGroup = (blockly) => (isGroup) => () =>
blockly.Events.setGroup(isGroup);

export const inputList = (block) => {
return block.inputList;
};

export const connectToPrevious = (block) => (input) => () => {
block.previousConnection.connect(input.connection);
};
export const previousConnection = (block) => {
return block.previousConnection;
};

export const nextConnection = (block) => {
return block.nextConnection;
};

export const connect = (from) => (to) => () => {
from.connect(to);
};

export const connectToOutput = (block) => (input) => () => {
block.outputConnection.connect(input.connection);
};

export const newBlock = (workspace) => (name) => () => {
var block = workspace.newBlock(name);
block.initSvg();
return block;
};

export const inputName = (input) => {
return input.name;
};

export const inputType = (input) => {
return input.type;
};

export const clearWorkspace = (workspace) => () => {
workspace.clear();
};

export const fieldRow = (input) => {
return input.fieldRow;
};

export const setFieldText = (field) => (text) => () => {
field.setValue(text);
};

export const fieldName = (field) => {
return field.name;
};
import * as internal from "src/Blockly/Internal.ts";

export const createBlocklyInstance_ = internal.createBlocklyInstance_;
export const addBlockType_ = internal.addBlockType_;
export const addChangeListener = internal.addChangeListener;
export const centerOnBlock = internal.centerOnBlock;
export const clearUndoStack = internal.clearUndoStack;
export const clearWorkspace = internal.clearWorkspace;
export const connect = internal.connect;
export const connectToOutput = internal.connectToOutput;
export const connectToPrevious = internal.connectToPrevious;
export const createWorkspace = internal.createWorkspace;
export const debugBlockly = internal.debugBlockly;
export const fieldName = internal.fieldName;
export const fieldRow = internal.fieldRow;
export const getBlockById_ = internal.getBlockById_;
export const getBlockType = internal.getBlockType;
export const hideChaff = internal.hideChaff;
export const initializeWorkspace_ = internal.initializeWorkspace_;
export const inputList = internal.inputList;
export const inputName = internal.inputName;
export const inputType = internal.inputType;
export const isWorkspaceEmpty = internal.isWorkspaceEmpty;
export const loadWorkspace = internal.loadWorkspace;
export const newBlock = internal.newBlock;
export const nextConnection = internal.nextConnection;
export const previousConnection = internal.previousConnection;
export const removeChangeListener = internal.removeChangeListener;
export const render = internal.render;
export const resize = internal.resize;
export const select = internal.select;
export const setFieldText = internal.setFieldText;
export const setGroup = internal.setGroup;
export const updateToolbox_ = internal.updateToolbox_;
export const workspaceToDom = internal.workspaceToDom;
export const workspaceXML = internal.workspaceXML;
Loading

0 comments on commit c7446fc

Please sign in to comment.