Last active
August 20, 2018 18:19
-
-
Save andreyselin/fcd21147b7cd609022d8fe97f4873416 to your computer and use it in GitHub Desktop.
Simple excel implementation on JS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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