Skip to content

Instantly share code, notes, and snippets.

@andreyselin
Last active August 20, 2018 18:19
Show Gist options
  • Save andreyselin/fcd21147b7cd609022d8fe97f4873416 to your computer and use it in GitHub Desktop.
Save andreyselin/fcd21147b7cd609022d8fe97f4873416 to your computer and use it in GitHub Desktop.
Simple excel implementation on JS
<html><head>
<style>
.workflow {
background: #f0f0f0;
position: relative;
}
.workflow input {
position: absolute;
}
.workflow input:hover {
border-color: cyan;
}
.workflow div {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
background: silver;
}
</style>
</head><body></body><script>
var regexps = {
isOperator: el => ['+','-','*','/'].indexOf(el)>-1,
isUsualOperator: el => ['+','-'].indexOf(el)>-1,
isDeepOperator: el => ['*','/'].indexOf(el)>-1,
isLink: new RegExp(/^[A-Za-z]+[0-9]+$/),
isNumber: new RegExp(/^([-0-9]+)$/),
splitLink: new RegExp(/([-0-9])+$/),
operandsAndOperators: new RegExp(/(?<![\+|\-|\*|\/])([\+|\-|\*|\/])/g)
}
class Field{
constructor (params) {
this.createElement = this.createElement.bind(this);
this.updateAncestors = this.updateAncestors.bind(this);
this.calculateThisAndDescendants = this.calculateThisAndDescendants.bind(this);
this.workflow = params.workflow;
this.col = params.col;
this.row = params.row;
this.element = null; // DOM Node
this.link = this.workflow.colNames[this.col] + (this.row+1);
this.rawValue = ""; //
this.isFormula = false;
// this.operands = []; // to del
this.ancestors = [];
this.descendants = [];
this.digitalValue = null;
// Execution:
this.createElement();
}
createElement () {
this.element = document.createElement("input");
this.element.setAttribute('type', 'text');
this.element.setAttribute('value', '');
this.element.style.display = "block";
this.element.style.position = "absolute";
this.element.style.top = (this.row+1) * this.workflow.rowHeight + "px";
this.element.style.left = (this.col+1) * this.workflow.colWidth + "px";
this.element.style.width = this.workflow.colWidth + "px";
this.element.style.height = this.workflow.rowHeight + "px";
this.element.onblur = () => {
this.value = this.element.value;
this.element.value = this.digitalValue;
};
this.element.onfocus = () => {
this.element.value = this.rawValue;
};
}
get value () {
if (this.digitalValue === null) throw new Error("NaN");
return this.digitalValue;
}
// Updates value in manual mode
set value (newValue) {
// set value
console.log("set value", newValue);
this.rawValue = newValue;
this.calculateThisAndDescendants();
// this.workflow.parseValue(newValue, this);
}
updateAncestors (newAncestors) {
newAncestors.forEach(newAncestor=>{
if (this.ancestors.indexOf(newAncestor)===-1) {
// Add new ancestor
if (this.workflow.getFieldByLink(newAncestor).descendants.indexOf(this.link)===-1) {
this.workflow.getFieldByLink(newAncestor).descendants.push(this.link);
}
}
});
this.ancestors.forEach(oldAncestor=>{
if (newAncestors.indexOf(oldAncestor)===-1) {
// Remove from ancestor's descendants
this.workflow.getFieldByLink(oldAncestor).descendants.splice(this.workflow.getFieldByLink(oldAncestor).descendants.indexOf(oldAncestor), 1);
}
});
this.ancestors = newAncestors;
}
calculateThisAndDescendants () {
this.workflow.parseValue(this.rawValue, this);
this.element.value = this.digitalValue;
this.descendants.forEach(descendant => this.workflow.getFieldByLink(descendant).calculateThisAndDescendants());
}
}
class Workflow {
constructor () {
this.generate = this.generate.bind(this);
this.parseValue = this.parseValue.bind(this);
this.getFieldByLink = this.getFieldByLink.bind(this);
this.addNameElement = this.addNameElement.bind(this);
this.setColName = this.setColName.bind(this);
this.fields = [];
this.activeCell = null;
this.selectMode = false;
this.cols=10;
this.rows=10;
this.colNames = [];
this.colWidth = 50;
this.rowHeight = 30;
// Execution:
this.generate();
}
//
generate () {
this.element = document.createElement("div");
this.element.setAttribute("class", "workflow");
this.element.style.width = (this.cols+1) * this.colWidth + "px";
this.element.style.height = (this.rows+1) * this.rowHeight + "px";
let c, r;
// Row names
for (r=0; r<this.rows; r++){
this.addNameElement(r, "row");
}
// Col names and fields
for (c=0; c<this.cols; c++) {
this.fields[c] = [];
this.addNameElement(c, "col");
this.colNames.push(this.setColName(c));
for (r=0; r<this.rows; r++){
this.fields[c][r] = new Field({workflow:this, col:c, row:r});
this.element.appendChild(this.fields[c][r].element);
}
}
document.body.appendChild(this.element);
}
addNameElement(num, type) {
let newNameElem = document.createElement("div");
newNameElem.style.width = this.colWidth + "px";
newNameElem.style.height = this.rowHeight + "px";
newNameElem.style.left = type === "col" ? this.colWidth*(num+1)+"px" : "0";
newNameElem.style.top = type === "row" ? this.rowHeight*(num+1)+"px" : "0";
newNameElem.innerText = type === "col" ? this.setColName(num) : num+1;
this.element.appendChild(newNameElem);
}
setColName (num) {
for (var ret = '', a = 1, b = 26; (num -= a-1) >= 0; a = b, b *= 26) {
ret = String.fromCharCode(parseInt((num % b) / a) + 65) + ret;
}
return ret;
}
getFieldByLink (link) {
var dims = link.split(regexps.splitLink);
return this.fields[this.colNames.indexOf(dims[0].toUpperCase())][parseInt(dims[1])-1];
}
parseValue (inputString, field) {
// console.log("inputString", inputString, regexps.isNumber.test(inputString));
// Parsing and executing expression
if (inputString.charAt(0) === "=") {
let newAncestors = [];
inputString = inputString.substr(1); // Removing "="
let executeExpression = function (theExpression){
let ops = {
'+':(a,b)=>a+b,
'-':(a,b)=>a-b,
'*':(a,b)=>a*b,
'/':(a,b)=>a/b
};
let result = theExpression[0],
i = 2;
while (i < theExpression.length) {
result = ops[theExpression[i-1]](result, theExpression[i]);
i+=2;
}
return result;
}.bind(this);
let addToExpression = function(elem, expressionToAddTo) {
if (typeof elem === "undefined") return; // For last iteration
expressionToAddTo = expressionToAddTo ? expressionToAddTo : expression;
if (regexps.isOperator(elem)) {
expressionToAddTo.push(elem);
} else if (regexps.isNumber.test(elem)) {
expressionToAddTo.push(parseFloat(elem));
} else if (regexps.isLink.test(elem)) {
expressionToAddTo.push(this.getFieldByLink(elem).value);
newAncestors.push(elem);
} else {
}
}.bind(this);
var expression = [];
var deepExpression = [];
var isDeep = false;
// Splits the string into arguments and operators
var elements = inputString.split (regexps.operandsAndOperators);
let elem = 1; //first operator
while (elem<=elements.length) { // <= to enable last calculation
// try {
// Adding or substracting operators
if (regexps.isUsualOperator(elements[elem]) || typeof elements[elem] === "undefined") {
if (isDeep) {
// Closing deep expression and executing it
addToExpression(elements[elem-1], deepExpression);
addToExpression(executeExpression (deepExpression));
addToExpression(elements[elem]);
isDeep = false;
} else {
addToExpression(elements[elem-1]);
addToExpression(elements[elem]);
}
// Multiplying or dividing operators
} else if (regexps.isDeepOperator(elements[elem]) || typeof elements[elem] === "undefined") {
if (isDeep) {
// Proceed within deep expression
addToExpression(elements[elem-1], deepExpression);
addToExpression(elements[elem], deepExpression);
} else {
// Create deepExpression
deepExpression = [];
addToExpression(elements[elem-1], deepExpression);
addToExpression(elements[elem], deepExpression);
isDeep = true;
}
}
elem += 2;
// } catch(e) {
// console.warn(e);
// break;
// }
}
field.updateAncestors(newAncestors);
field.digitalValue = executeExpression (expression);
} else if (regexps.isNumber.test(inputString)) {
field.digitalValue = parseFloat(inputString);
} else {
field.digitalValue = null;
}
}
}
var workflow = new Workflow();
</script></html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment