Skip to content

Instantly share code, notes, and snippets.

@madacol
Last active March 22, 2022 07:42
Show Gist options
  • Save madacol/8973fc40566a61dcf73cc92d68bd5ec6 to your computer and use it in GitHub Desktop.
Save madacol/8973fc40566a61dcf73cc92d68bd5ec6 to your computer and use it in GitHub Desktop.
Simple javascript spreadsheet
<!DOCTYPE html>
<html>
<head>
<script>
/**
* This is a modified version of the spreadsheet below ↓ to allow editing while receiving realtime changes
* https://github.com/xem/sheet/blob/gh-pages/full-commented.html
*/
// Sheet!
// ======
// a sub-256b full-featured JS spreadsheet
// =======================================
// This app creates a spreadsheet of 4 x 6 cells supporting:
// - any value: text, number or float (ex: `Hi!` or `8` or `1.234`).
// - formulae (ex: `=A1+8`), visible when a cell is focused and executed on blur.
// - implicit `Math` in formulae (ex: `=sqrt(A1)`).
// - updates all the grid in cascade when a cell value changes.
// - protection against auto or circular references.
// - localStorage persistence.
// We start by declaring the function `o`. This function has two uses:
// - If it's called with an argument `b`, it will initialize and draw all the spreadsheet in HTML.
// - It it's called with no argument, it will evaluate all the cells and update their values.
o = b => {
// This weird `for` statement is used to make a loop that iterates a lot of times, by using very few characters.
// `{} + o` generates a 245-chars string containing the String value of `{}` ("[object Object]"),
// followed by all the source code of `o`. ("b=>{for(i in{}+o) ... x-~x?+x:x)}").
// Minified, `for(i in{}+o)` is 5 chars shorter than the equivalent `for(i=0;i<245;i++)`.
// `i` is the loop var going from 0 to 245.
for(i in {} + o){
// All the following code is executed with `Math` implied.
// This allows to call Math functions in formulae, without writing "Math.",
// but the Math object (`M`) will also be used to store all the cells values before they're evaluated (especially the formulae).
with(M = Math){
// The string `y` represents the current cell's name.
// It is built by concatenating the (i%5)'th char of 'ABCD',
// and `-~(i/5%6)`, which is equivalent to `1 + Math.floor(((i / 5) % 6))` (this is needed to name the first line "1" instead of "0").
// So in each loop, `y` gets a value from this list: "A1", "B1", "C1", "D1", NaN, "A2", "B2", ..., "C6", "D6", NaN, then it gets back to "A1" , "A2", ...
// We then store data in `M[y]`, according to the value of `b`:
M[y = 'ABCD'[i % 5] + -~(i / 5 % 6)] =
// If `b` is set:
b ?
// We use the first 30 values of `y` to render the grid in HTML:
document.write(
(
// If `y` is truthy and lower than 30:
// (the limit of 30 ensures that the grid keeps a 24 cells + 6 line breaks)
y && i < 30
// write the beginning of an <input> tag, with the attributes "placeholder" and "id" equal to `y`.
? `<input placeholder=${y} id=` + y
// if `y` is NaN, write the beginning of a <br> tag instead, to pass on a new line.
: '<br'
),
// then write the end of the current tag, including two event listeners:
//
// `onblur=l[id]=value;o()`:
// as soon as a cell loses focus,
// this event will store the value of the cell (`this.value`) in `l[this.id]` (`l` represents `localStorage`, and the two `this` are implicit),
// then it will call the function `o()` with no argument to evaluate all the cells.
//
// `onfocus=value=[l[id]]`:
// as soon as the cell is focused,
// this event will put the stored cell value (`l[this.id]`) in `this.value` (so the user can see his original formula).
// `l[this.id]` is surrounded by an array (`[]`) to avoid writing "undefined" when we focus an empty cell.
// indeed, the array `[undefined]` coerces into an empty string, which is what we want here (keep the empty cells empty on focus).
` onfocus=value=[l[id]] onblur=l[id]=value,o()>`
// You may notice that the <br>'s also get an id, a placeholder and two event listeners, but they don't have any use.
// It just saves a few chars to use the same ending for both <br> and <input> tags.
// The result of `document.write` (i.e. undefined) is stored in `M[y]`, which also has no effect.
// It just saves a few chars to use the same assignment `M[y]=` whether `b` is set or not.
)
// If `b` is not set:
:
// the loop is not limited to 30 iterations anymore, so the following code is executed 8 to 9 times for each cell of the spreadsheet.
// These passes allow to execute deeply nested formulae and avoid browser-freezing, infinite circular references,
// which could occur if we had executed each formula as soon as its cell's value changes.
(
// `top[y]` is the input tag with the id `y`. We're updating its content according to the following rules:
((document.activeElement !== top[y]) && (top[y].value = [
// If the stored value of the cell (called `z`) starts with a "=", then it's a formula.
// We use `/^=/.test(z)` here instead of `z[0]=="="` to avoid breaking when `l[y]` is undefined.
/^=/.test(z = l[y]) ?
// We eval the string `'x'+z`.
// Ex: if the stored formula of A1 is "=A2+2", then we eval the string 'x=A2+2".
// here, `A2` represents the value of the cell A2 which has been previously stored in `M`.
// Indeed, if we consider the surrounding `with(M=Math)`, the string we execute is equivalent to 'x=M.A2+2'.
// So, `x` contains the the saved value of `A2`, plus 2.
// If A2 contains a number, it'll work instantly.
// But if A2 contains a formula (for example "=A3+8", and A3 contains "5"), A1's `x` will not get its value instantly.
// For the cell A1, the computed value of `x` will get a garbage value, but for A2, it will get computed fine as 5+8 = 13.
// By chance, there are many "eval" passes on the grid's cells, and at the second pass, A1 will be evaluated correctly:
// This time, 'x=M.A2+2' will now equal 'x=13+2' and `x` will contain the number 15.
eval('x' + z)
:
// If `z` is not a formula (i.e. number or text), it's put it directly in x, as a string.
x = z
])),
// Now it's time to decide what to store in `M[y]`:
// if (x-~x) is not truthy, it means that x is a string representing an integer or a float.
// We use this formula instead of "+x" to also support the number "0" (because "0"-~"0" == 1)
x - ~x ?
// In this case, we store `+x` (similar to `parseFloat(x)`) in `M[y]`.
+x
// Else, if `x` contains text:
:
// We store that text in `M[y]`.
x
)
// When a parenthesis contains many statements like here, all are executed, but only the last one is returned,
// That's why `M[y]=(..., x-~x?+x:x)` successfully stores `+x` or `x` in `M[y]`
};
}
}
// Finally, let's declare `l` as a shortcut for `localStorage`,
// then call `o()` a first time with `l` as argument. `localStorage` is truthy, so `o` will draw the grid.
// Then call `o()` a second time with the return of the first call as argument.
// But `o()` returns `undefined`, so this time, all the cells are filled with the values stored in localStorage and all the formulae are evaluated.
o(
o(
l = localStorage
)
)
// The End
</script>
</head>
<body>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment