title: World smallest office suite
url: https://zserge.com/posts/awfice/
hash_url: 3ac474db4dc65c1d25e99cb30655ff12

<p>We are all familiar with a traditional office suite - a word processor, a spreadsheet, a presentation program, maybe a diagramming or note-taking app. We have seen it all in Microsoft Office and Google Docs. Those are really powerful and large. But what would be the most minimal amount of code required to build an office suite?</p><h2 id="platform">platform</h2><p>Obviously, our office suite won’t be a desktop GUI app - those require plenty of code and efforts to build one. Same applies to native mobile apps. We might consider building a console (terminal-based) app, and in fact there are already absurdly small <a href="https://github.com/antirez/kilo">text editors</a> or <a href="https://github.com/c00kiemon5ter/ioccc-obfuscated-c-contest/blob/master/2000/jarijyrki.c">spreadsheets</a>, but it would be much easier if we targeted a browser.</p><p>Browsers already come with a decent rich-text editor (contenteditable) and are really good (although, unsafe) <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval">evaluator</a> for math expressions.</p><p>Now, how small can we get?</p><h2 id="text-editor">text editor</h2><p>This is in fact the “app” I have been using for years:</p><p>Yes, that’s it. Moreover, one can turn it into a self-contained URL and that’s how I use it when I need a scratchpad for quick notes:</p><div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html">data:text/html,<span class="p">&lt;</span><span class="nt">html</span> <span class="na">contenteditable</span><span class="p">&gt;</span>
</code></pre></div><p>Try pasting this into your URL bar. If your browser is nice with you, you should be able to use <code>Ctrl+B</code> or <code>Ctrl+I</code> to make text look bold or italic.</p><p>We can enhance it a bit more by adding some style (yes, I believe that <a href="http://bettermotherfuckingwebsite.com/">some small typographic improvements</a> matter):</p><div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html">data:text/html,<span class="p">&lt;</span><span class="nt">body</span> <span class="na">contenteditable</span> <span class="na">style</span><span class="o">=</span><span class="s">"line-height:1.5;font-size:20px;"</span><span class="p">&gt;</span>
</code></pre></div><p>I have added this to bookmarks and now my zero-weight text editor is one keypress away from me. You might also use it as a temporary clipboard to paste text or even pictures.</p><p>Of course, this can be further extended to support various heading styles, lists or indentation.</p><p>You can save your text by saving the whole HTML as a file, or by printing it on paper.</p><h2 id="presentations">presentations</h2><p>Some time ago I already made a <a href="https://github.com/trikita/slide-html">simple presentation tool</a>, which is a self-contained HTML file that one can edit (like markdown text) and it will render in a colorful <a href="https://en.wikipedia.org/wiki/Takahashi_method">Takahashi-style</a> presentation.</p><p>This time, as we continue talking about <code>contenteditable</code>, we will make a WYSYWIG slide editor instead. First of all, let’s create several empty slides that are editable:</p><div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;&lt;</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="nx">i</span><span class="o">&lt;</span><span class="mi">50</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
  <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">+=</span> <span class="sb">`
</span><span class="sb">    &lt;div style="position:relative;width:90%;padding-top:60%;margin:5%;border:1px solid silver;page-break-after:always;"&gt;
</span><span class="sb">      &lt;div contenteditable style="outline:none;position:absolute;right:10%;bottom:10%;left:10%;top:10%;font-size:5vmin;"&gt;
</span><span class="sb">      &lt;/div&gt;
</span><span class="sb">    &lt;/div&gt;`</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</code></pre></div><p>The number of 50 is arbitrary, but I don’t remember ever using more slides than this. Each outer div is a slide with a thin outline. The trick with width and padding-top is to keep the slide aspect ratio. Try changing the values to see how that affects the layout. Each inner div is a basic rich text editor, with a fairly large font to be readable from the projector screen.</p><p>Good enough. But we want to have headers and lists on our slides, don’t we?</p><p>Let’s add some hotkeys:</p><div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">"div&gt;div"</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="p">{</span>
  <span class="nx">el</span><span class="p">.</span><span class="nx">onkeydown</span> <span class="o">=</span> <span class="nx">e</span><span class="p">=&gt;</span> <span class="p">{</span>
    <span class="c1">// `code` will be false if Ctrl or Alt are not pressed
</span><span class="c1"></span>    <span class="c1">// `code` will be 0..8 for numeric keys 1..9
</span><span class="c1"></span>    <span class="c1">// `code` will be some other numeric value if another key is pressed
</span><span class="c1"></span>    <span class="c1">// with Ctrl+Alt hold.
</span><span class="c1"></span>    <span class="kr">const</span> <span class="nx">code</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">ctrlKey</span> <span class="o">&amp;&amp;</span> <span class="nx">e</span><span class="p">.</span><span class="nx">altKey</span> <span class="o">&amp;&amp;</span> <span class="nx">e</span><span class="p">.</span><span class="nx">keyCode</span><span class="o">-</span><span class="mi">49</span><span class="p">;</span>
    <span class="c1">// Find the suitable rich text command, or undefined if the key
</span><span class="c1"></span>    <span class="c1">// is out of range
</span><span class="c1"></span>    <span class="nx">x</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"formatBlock"</span><span class="p">,</span> <span class="s2">"formatBlock"</span><span class="p">,</span> <span class="s2">"justifyLeft"</span><span class="p">,</span> <span class="s2">"justifyCenter"</span><span class="p">,</span> 
         <span class="s2">"justifyRight"</span><span class="p">,</span> <span class="s2">"outdent"</span><span class="p">,</span> <span class="s2">"indent"</span><span class="p">,</span> <span class="s2">"insertUnorderedList"</span><span class="p">][</span><span class="nx">n</span><span class="p">];</span>
    <span class="c1">// Find command parameter (only for 1 and 2)
</span><span class="c1"></span>    <span class="nx">y</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"&lt;h1&gt;"</span><span class="p">,</span> <span class="s2">"&lt;div&gt;"</span><span class="p">][</span><span class="nx">n</span><span class="p">];</span>
    <span class="c1">// Send the command and the parameter (if any) to the editor
</span><span class="c1"></span>    <span class="k">if</span> <span class="p">(</span><span class="nx">x</span><span class="p">)</span> <span class="p">{</span>
      <span class="nb">document</span><span class="p">.</span><span class="nx">execCommand</span><span class="p">(</span><span class="nx">x</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="nx">y</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">};</span>
<span class="p">});</span>
</code></pre></div><p>Now if we press <code>Ctrl+Alt+1</code> inside the slide - we make the selected text a header. Or if we press <code>Ctrl+Alt+2</code> we turn it back to normal. <code>Ctrl+Alt+3</code>..<code>Ctrl+Alt+5</code> change the alignment, and <code>6</code> and <code>7</code> change the indentation. <code>8</code> starts a list. <code>9</code> is left for your own needs, feel free to customize. The full list of <code>contenteditable</code> operations can be found on <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand">MDN</a>.</p><p>Squeezing the code above a little bit and turning it into a data URL would result in the following ~600 bytes long rich slide editor:</p><div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html">data:text/html,<span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;@</span><span class="k">page</span><span class="p">{</span><span class="nt">size</span><span class="o">:</span> <span class="nt">6in</span> <span class="nt">8in</span> <span class="nt">landscape</span><span class="o">;</span><span class="p">}&lt;/</span><span class="nt">style</span><span class="p">&gt;&lt;</span><span class="nt">body</span><span class="p">&gt;&lt;</span><span class="nt">script</span><span class="p">&gt;</span><span class="nx">d</span><span class="o">=</span><span class="nb">document</span><span class="p">;</span><span class="k">for</span><span class="p">(</span><span class="nx">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span><span class="nx">i</span><span class="o">&lt;</span><span class="mi">50</span><span class="p">;</span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="nx">d</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">innerHTML</span><span class="o">+=</span><span class="s1">'&lt;div style="position:relative;width:90%;padding-top:60%;margin:5%;border:1px solid silver;page-break-after:always;"&gt;&lt;div contenteditable style="outline:none;position:absolute;right:10%;bottom:10%;left:10%;top:10%;font-size:5vmin;"&gt;&lt;/div&gt;&lt;/div&gt;'</span><span class="p">;</span><span class="nx">d</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">"div&gt;div"</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">e</span><span class="p">=&gt;</span><span class="nx">e</span><span class="p">.</span><span class="nx">onkeydown</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;{</span><span class="nx">n</span><span class="o">=</span><span class="nx">e</span><span class="p">.</span><span class="nx">ctrlKey</span><span class="o">&amp;&amp;</span><span class="nx">e</span><span class="p">.</span><span class="nx">altKey</span><span class="o">&amp;&amp;</span><span class="nx">e</span><span class="p">.</span><span class="nx">keyCode</span><span class="o">-</span><span class="mi">49</span><span class="p">,</span><span class="nx">x</span><span class="o">=</span><span class="p">[</span><span class="s2">"formatBlock"</span><span class="p">,</span><span class="s2">"formatBlock"</span><span class="p">,</span><span class="s2">"justifyLeft"</span><span class="p">,</span><span class="s2">"justifyCenter"</span><span class="p">,</span><span class="s2">"justifyRight"</span><span class="p">,</span><span class="s2">"outdent"</span><span class="p">,</span><span class="s2">"indent"</span><span class="p">,</span><span class="s2">"insertUnorderedList"</span><span class="p">][</span><span class="nx">n</span><span class="p">],</span><span class="nx">y</span><span class="o">=</span><span class="p">[</span><span class="s2">"&lt;h1&gt;"</span><span class="p">,</span><span class="s2">"&lt;div&gt;"</span><span class="p">][</span><span class="nx">n</span><span class="p">],</span><span class="nx">x</span><span class="o">&amp;&amp;</span><span class="nb">document</span><span class="p">.</span><span class="nx">execCommand</span><span class="p">(</span><span class="nx">x</span><span class="p">,</span><span class="o">!</span><span class="mi">1</span><span class="p">,</span><span class="nx">y</span><span class="p">)})&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</code></pre></div><p>The slides can be exported to PDF by printing into the file, and from there could be shown on any computer.</p><h2 id="quick-drawing">quick drawing</h2><p>A while ago I’ve built <a href="https://onthesamepage.online">https://onthesamepage.online</a> to quickly sketch ideas in collaboration with other people, but despite its simplicity it’s still larger than what we do here.</p><p>As a bare minimum, we can only draw lines on canvas. We need a <code>&lt;canvas&gt;</code> elements, a few mouse/touch handlers and a flag to indicate that mouse movement is actually drawing when the mouse is pressed.</p><p>Here worth mentioning that elements with an id can be accessed as window[id] or window.id. The thing that wasn’t standardized for a long time and has been a hack from IE, now has become a <a href="https://html.spec.whatwg.org/multipage/window-object.html#named-access-on-the-window-object">standard</a>.</p><p>Also, I moved cursor position handling to separate short functions to reuse them in mousedown and mousemove handlers. Finally, I reset the margins of the body elements to make our canvas full screen.</p><p>The minified code is roughly 400 bytes and allows you to draw with your mouse, nothing more, nothing less:</p><div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="o">&lt;</span><span class="nx">canvas</span> <span class="nx">id</span><span class="o">=</span><span class="s2">"v"</span><span class="o">&gt;</span>
<span class="o">&lt;</span><span class="nx">script</span><span class="o">&gt;</span>
<span class="nx">d</span><span class="o">=</span><span class="nb">document</span><span class="p">,</span> <span class="c1">// shortcut for document
</span><span class="c1"></span><span class="nx">d</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">margin</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="c1">// reset style
</span><span class="c1"></span><span class="nx">f</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="c1">// mouse-down flag
</span><span class="c1"></span><span class="nx">c</span><span class="o">=</span><span class="nx">v</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="s2">"2d"</span><span class="p">),</span> <span class="c1">// canvas context
</span><span class="c1"></span><span class="nx">v</span><span class="p">.</span><span class="nx">width</span><span class="o">=</span><span class="nx">innerWidth</span><span class="p">,</span> <span class="c1">// make canvas element fullscreen
</span><span class="c1"></span><span class="nx">v</span><span class="p">.</span><span class="nx">height</span><span class="o">=</span><span class="nx">innerHeight</span><span class="p">,</span>
<span class="nx">c</span><span class="p">.</span><span class="nx">lineWidth</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="c1">// make lines a bit wider
</span><span class="c1"></span><span class="nx">x</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;</span><span class="nx">e</span><span class="p">.</span><span class="nx">clientX</span><span class="o">||</span><span class="nx">e</span><span class="p">.</span><span class="nx">touches</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">clientX</span><span class="p">,</span> <span class="c1">// get X position from mouse/touch
</span><span class="c1"></span><span class="nx">y</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;</span><span class="nx">e</span><span class="p">.</span><span class="nx">clientY</span><span class="o">||</span><span class="nx">e</span><span class="p">.</span><span class="nx">touches</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">clientY</span><span class="p">,</span> <span class="c1">// get Y position from mouse/touch
</span><span class="c1"></span><span class="nx">d</span><span class="p">.</span><span class="nx">onmousedown</span><span class="o">=</span><span class="nx">d</span><span class="p">.</span><span class="nx">ontouchstart</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;{</span><span class="nx">f</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span><span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">(),</span><span class="nx">c</span><span class="p">.</span><span class="nx">moveTo</span><span class="p">(</span><span class="nx">x</span><span class="p">(</span><span class="nx">e</span><span class="p">),</span><span class="nx">y</span><span class="p">(</span><span class="nx">e</span><span class="p">)),</span><span class="nx">c</span><span class="p">.</span><span class="nx">beginPath</span><span class="p">()},</span>
<span class="nx">d</span><span class="p">.</span><span class="nx">onmousemove</span><span class="o">=</span><span class="nx">d</span><span class="p">.</span><span class="nx">ontouchmove</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;{</span><span class="nx">f</span><span class="o">&amp;&amp;</span><span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">lineTo</span><span class="p">(</span><span class="nx">x</span><span class="p">(</span><span class="nx">e</span><span class="p">),</span><span class="nx">y</span><span class="p">(</span><span class="nx">e</span><span class="p">)),</span><span class="nx">c</span><span class="p">.</span><span class="nx">stroke</span><span class="p">())},</span>
<span class="nx">d</span><span class="p">.</span><span class="nx">onmouseup</span><span class="o">=</span><span class="nx">d</span><span class="p">.</span><span class="nx">ontouchend</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;</span><span class="nx">f</span><span class="o">=</span><span class="mi">0</span>
<span class="o">&lt;</span><span class="err">/script&gt;</span>
</code></pre></div><p>Or, as a short one-liner bookmark:</p><div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">data</span><span class="o">:</span><span class="nx">text</span><span class="o">/</span><span class="nx">html</span><span class="p">,</span><span class="o">&lt;</span><span class="nx">canvas</span> <span class="nx">id</span><span class="o">=</span><span class="s2">"v"</span><span class="o">&gt;&lt;</span><span class="nx">script</span><span class="o">&gt;</span><span class="nx">d</span><span class="o">=</span><span class="nb">document</span><span class="p">,</span><span class="nx">d</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">margin</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span><span class="nx">f</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span><span class="nx">c</span><span class="o">=</span><span class="nx">v</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="s2">"2d"</span><span class="p">),</span><span class="nx">v</span><span class="p">.</span><span class="nx">width</span><span class="o">=</span><span class="nx">innerWidth</span><span class="p">,</span><span class="nx">v</span><span class="p">.</span><span class="nx">height</span><span class="o">=</span><span class="nx">innerHeight</span><span class="p">,</span><span class="nx">c</span><span class="p">.</span><span class="nx">lineWidth</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span><span class="nx">x</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;</span><span class="nx">e</span><span class="p">.</span><span class="nx">clientX</span><span class="o">||</span><span class="nx">e</span><span class="p">.</span><span class="nx">touches</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">clientX</span><span class="p">,</span><span class="nx">y</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;</span><span class="nx">e</span><span class="p">.</span><span class="nx">clientY</span><span class="o">||</span><span class="nx">e</span><span class="p">.</span><span class="nx">touches</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">clientY</span><span class="p">,</span><span class="nx">d</span><span class="p">.</span><span class="nx">onmousedown</span><span class="o">=</span><span class="nx">d</span><span class="p">.</span><span class="nx">ontouchstart</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;{</span><span class="nx">f</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span><span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">(),</span><span class="nx">c</span><span class="p">.</span><span class="nx">moveTo</span><span class="p">(</span><span class="nx">x</span><span class="p">(</span><span class="nx">e</span><span class="p">),</span><span class="nx">y</span><span class="p">(</span><span class="nx">e</span><span class="p">)),</span><span class="nx">c</span><span class="p">.</span><span class="nx">beginPath</span><span class="p">()},</span><span class="nx">d</span><span class="p">.</span><span class="nx">onmousemove</span><span class="o">=</span><span class="nx">d</span><span class="p">.</span><span class="nx">ontouchmove</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;{</span><span class="nx">f</span><span class="o">&amp;&amp;</span><span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">lineTo</span><span class="p">(</span><span class="nx">x</span><span class="p">(</span><span class="nx">e</span><span class="p">),</span><span class="nx">y</span><span class="p">(</span><span class="nx">e</span><span class="p">)),</span><span class="nx">c</span><span class="p">.</span><span class="nx">stroke</span><span class="p">())},</span><span class="nx">d</span><span class="p">.</span><span class="nx">onmouseup</span><span class="o">=</span><span class="nx">d</span><span class="p">.</span><span class="nx">ontouchend</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;</span><span class="nx">f</span><span class="o">=</span><span class="mi">0</span><span class="o">&lt;</span><span class="err">/script&gt;</span>
</code></pre></div><h2 id="spreadsheet">spreadsheet</h2><p>This would probably be the most complex one and the largest one, but we will try to stay below the limit of 1KB per app.</p><p>The layout would be simple. HTML comes with tables, so why don’t we use them. As the spreadsheet cells are normally addressable by “letter” + “number”, let’s restrict our table to 26x100 cells. It makes sense to create rows and cells dynamically in a loop. Some basic styling would make our spreadsheet look nicer:</p><div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="o">&lt;</span><span class="nx">table</span> <span class="nx">id</span><span class="o">=</span><span class="s2">"t"</span><span class="o">&gt;</span>

<span class="nx">t</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">borderCollapse</span><span class="o">=</span><span class="s2">"collapse"</span><span class="p">;</span> <span class="c1">// remove gaps between cells
</span><span class="c1"></span><span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">101</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
  <span class="kr">const</span> <span class="nx">row</span> <span class="o">=</span> <span class="nx">t</span><span class="p">.</span><span class="nx">insertRow</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="mi">27</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// convert column index j to a letter (char code of "A" is 65)
</span><span class="c1"></span>    <span class="kr">const</span> <span class="nx">letter</span> <span class="o">=</span> <span class="nb">String</span><span class="p">.</span><span class="nx">fromCharCode</span><span class="p">(</span><span class="mi">65</span><span class="o">+</span><span class="nx">j</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span> <span class="c1">// 1=A, 2=B, 3=C etc
</span><span class="c1"></span>    <span class="kr">const</span> <span class="nx">cell</span> <span class="o">=</span> <span class="nx">row</span><span class="p">.</span><span class="nx">insertCell</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
    <span class="nx">cell</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">border</span> <span class="o">=</span> <span class="s2">"1px solid silver"</span><span class="p">;</span> <span class="c1">// make thin grey border
</span><span class="c1"></span>    <span class="nx">cell</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">textAlign</span> <span class="o">=</span> <span class="s2">"right"</span><span class="p">;</span>         <span class="c1">// right-align, like excel
</span><span class="c1"></span>    <span class="k">if</span> <span class="p">(</span><span class="nx">i</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="nx">j</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// add identifiable input field, this is where formula is entered
</span><span class="c1"></span>      <span class="kr">const</span> <span class="nx">field</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'input'</span><span class="p">);</span>
      <span class="nx">field</span><span class="p">.</span><span class="nx">id</span> <span class="o">=</span> <span class="nx">letter</span> <span class="o">+</span> <span class="nx">i</span><span class="p">;</span> <span class="c1">// i.e, "B3"
</span><span class="c1"></span>      <span class="nx">cell</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">field</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">i</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// Row numbers
</span><span class="c1"></span>      <span class="nx">cell</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">i</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">j</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// Column letters
</span><span class="c1"></span>      <span class="nx">cell</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">letter</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>Now we have a large grid of cells, with rows and columns. Time to add an expression evaluator. We can achieve a hacky, but mostly working evaluator with 3 arrays - an array of all input fields (to get their actual entered values, number or formulas), an array that has a smart getter, calling eval() if a variable is requested and it is linked to an input field with the formula, and a cache of the last entered values for each field:</p><div class="highlight"><pre class="chroma"><code class="language-fallback" data-lang="fallback">inputs = []; // assume that we did inputs.push(field) for each field in the loop above
data = {}; // smart data accessing object
cache = {}; // cache

// Re-calculate all fields
const calc = () =&gt; {
  inputs.map(field =&gt; {
    try {
      field.value = D[field.id];
    } catch (e) { /* ignore */}
  });
};

// We also need to customize our field initialization code:
field.onfocus = () =&gt; {
  // When element is focused - replace its calculated value with its formula
  field.value = cache[field.id] || "";
};
field.onblur = () =&gt; {
  // When element loses focus - put formula in cache, and re-calculate everything
  cache[field.id] = field.value;
  calc();
};
// Smart getter for a field, evaluates formula if needed
const get = () =&gt; {
  let value = cache[field.id] || "";
  if(value.chatAt(0) == "=") {
    // evaluate the formula using "with" hack:
    with(data) return eval(value.substring(1));
  } else {
    // return value as it is, convert to number if possible:
    return isNaN(parseFloat(value)) ? value : parseFloat(value);
  }
};
// Add smart getter to the data array for both upper and lower case variants:
Object.defineProperty(data, field.id, {get}),
Object.defineProperty(data, field.id.toLowerCase(), {get})
</code></pre></div><p>Now the spreadsheet should work, if you put, for example, “42” into A1 and “=A1+3” into A2 - you should see “45” when you move the focus away from A2.</p><p>After carefully minizing the code above, we get the following ~800 byte working spreadsheet:</p><div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">data</span><span class="o">:</span><span class="nx">text</span><span class="o">/</span><span class="nx">html</span><span class="p">,</span><span class="o">&lt;</span><span class="nx">table</span> <span class="nx">id</span><span class="o">=</span><span class="s2">"t"</span><span class="o">&gt;&lt;</span><span class="nx">script</span><span class="o">&gt;</span><span class="k">for</span><span class="p">(</span><span class="nx">I</span><span class="o">=</span><span class="p">[],</span><span class="nx">D</span><span class="o">=</span><span class="p">{},</span><span class="nx">C</span><span class="o">=</span><span class="p">{},</span><span class="nx">calc</span><span class="o">=</span><span class="p">()=&gt;</span><span class="nx">I</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">e</span><span class="p">=&gt;{</span><span class="k">try</span><span class="p">{</span><span class="nx">e</span><span class="p">.</span><span class="nx">value</span><span class="o">=</span><span class="nx">D</span><span class="p">[</span><span class="nx">e</span><span class="p">.</span><span class="nx">id</span><span class="p">]}</span><span class="k">catch</span><span class="p">(</span><span class="nx">e</span><span class="p">){}}),</span><span class="nx">t</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">borderCollapse</span><span class="o">=</span><span class="s2">"collapse"</span><span class="p">,</span><span class="nx">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span><span class="nx">i</span><span class="o">&lt;</span><span class="mi">101</span><span class="p">;</span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="k">for</span><span class="p">(</span><span class="nx">r</span><span class="o">=</span><span class="nx">t</span><span class="p">.</span><span class="nx">insertRow</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">),</span><span class="nx">j</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span><span class="nx">j</span><span class="o">&lt;</span><span class="mi">27</span><span class="p">;</span><span class="nx">j</span><span class="o">++</span><span class="p">)</span><span class="nx">c</span><span class="o">=</span><span class="nb">String</span><span class="p">.</span><span class="nx">fromCharCode</span><span class="p">(</span><span class="mi">65</span><span class="o">+</span><span class="nx">j</span><span class="o">-</span><span class="mi">1</span><span class="p">),</span><span class="nx">d</span><span class="o">=</span><span class="nx">r</span><span class="p">.</span><span class="nx">insertCell</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">),</span><span class="nx">d</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">border</span><span class="o">=</span><span class="s2">"1px solid gray"</span><span class="p">,</span><span class="nx">d</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">textAlign</span><span class="o">=</span><span class="s2">"right"</span><span class="p">,</span><span class="nx">d</span><span class="p">.</span><span class="nx">innerHTML</span><span class="o">=</span><span class="nx">i</span><span class="o">?</span><span class="nx">j</span><span class="o">?</span><span class="s2">""</span><span class="o">:</span><span class="nx">i</span><span class="o">:</span><span class="nx">c</span><span class="p">,</span><span class="nx">i</span><span class="o">*</span><span class="nx">j</span><span class="o">&amp;&amp;</span><span class="nx">I</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">d</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">((</span><span class="nx">f</span><span class="p">=&gt;(</span><span class="nx">f</span><span class="p">.</span><span class="nx">id</span><span class="o">=</span><span class="nx">c</span><span class="o">+</span><span class="nx">i</span><span class="p">,</span><span class="nx">f</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">border</span><span class="o">=</span><span class="s2">"none"</span><span class="p">,</span><span class="nx">f</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">width</span><span class="o">=</span><span class="s2">"4rem"</span><span class="p">,</span><span class="nx">f</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">textAlign</span><span class="o">=</span><span class="s2">"right"</span><span class="p">,</span><span class="nx">f</span><span class="p">.</span><span class="nx">onfocus</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;</span><span class="nx">f</span><span class="p">.</span><span class="nx">value</span><span class="o">=</span><span class="nx">C</span><span class="p">[</span><span class="nx">f</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span><span class="o">||</span><span class="s2">""</span><span class="p">,</span><span class="nx">f</span><span class="p">.</span><span class="nx">onblur</span><span class="o">=</span><span class="nx">e</span><span class="p">=&gt;{</span><span class="nx">C</span><span class="p">[</span><span class="nx">f</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span><span class="o">=</span><span class="nx">f</span><span class="p">.</span><span class="nx">value</span><span class="p">,</span><span class="nx">calc</span><span class="p">()},</span><span class="nx">get</span><span class="o">=</span><span class="p">()=&gt;{</span><span class="kd">let</span> <span class="nx">v</span><span class="o">=</span><span class="nx">C</span><span class="p">[</span><span class="nx">f</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span><span class="o">||</span><span class="s2">""</span><span class="p">;</span><span class="k">if</span><span class="p">(</span><span class="s2">"="</span><span class="o">!=</span><span class="nx">v</span><span class="p">.</span><span class="nx">charAt</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span><span class="k">return</span> <span class="nb">isNaN</span><span class="p">(</span><span class="nb">parseFloat</span><span class="p">(</span><span class="nx">v</span><span class="p">))</span><span class="o">?</span><span class="nx">v</span><span class="o">:</span><span class="nb">parseFloat</span><span class="p">(</span><span class="nx">v</span><span class="p">);</span><span class="kd">with</span><span class="p">(</span><span class="nx">D</span><span class="p">)</span><span class="k">return</span> <span class="nb">eval</span><span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">1</span><span class="p">))},</span><span class="nb">Object</span><span class="p">.</span><span class="nx">defineProperty</span><span class="p">(</span><span class="nx">D</span><span class="p">,</span><span class="nx">f</span><span class="p">.</span><span class="nx">id</span><span class="p">,{</span><span class="nx">get</span><span class="o">:</span><span class="nx">get</span><span class="p">}),</span><span class="nb">Object</span><span class="p">.</span><span class="nx">defineProperty</span><span class="p">(</span><span class="nx">D</span><span class="p">,</span><span class="nx">f</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">(),{</span><span class="nx">get</span><span class="o">:</span><span class="nx">get</span><span class="p">}),</span><span class="nx">f</span><span class="p">))(</span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">"input"</span><span class="p">))))</span><span class="o">&lt;</span><span class="err">/script&gt;</span>
</code></pre></div><h2 id="are-you-serious">are you serious?</h2><p>Well, in no way it’s a replacement for a proper office suite. But it’s a good demonstration of minimalism and tiny code. All these apps are ephemeral, they lose their state if you refresh the page and it looks like there is no way for data URLs to keep their state. But they might be helpful as quick bookmarks if you need to calculate a few bits or draft some quick note without opening heavy “real” office apps. As a bonus, all these tiny apps are ultimately respectful to your privacy and do no share your data (do not store it either).</p><p>So, yes, it is more of a joke than a serious application, but still, I created a repo for these tiny apps in case anyone would like to use them or customize further for their own needs: <a href="http://github.com/zserge/awfice">http://github.com/zserge/awfice</a>. PRs and further improvements are welcome!</p><p>I hope you’ve enjoyed this article. You can follow – and contribute to – on <a href="https://github.com/zserge">Github</a>, <a href="https://twitter.com/zsergo">Twitter</a> or subscribe via <a href="https://zserge.com/rss.xml">rss</a>.</p><p class="date"><em>Oct 11, 2020</em></p>
