The smallest office suite in the world

We are all familiar with a traditional office suite – a word processor, spreadsheets, presentation software, perhaps an application for creating diagrams or notes. We see all this in Microsoft Office and in Google Docs. All of these programs are powerful and voluminous. But what is the minimum amount of code required to create an office suite?

Platform

Obviously, our office suite will not be a GUI desktop application – it takes a lot of code and work to create something like this. The same is true for native mobile apps. We may consider creating a console (terminal) application, and in fact there are absurdly small text editors or spreadsheets, but it will be much easier if we choose the browser as the platform.

Browsers already have a decent text editor with formatting (contenteditable) and they are very good (albeit insecure) calculators mathematical expressions.

How small can we make the package?

Text editor

In fact, this is the “app” I’ve been using for years:

<html contenteditable>

Yes, that’s all. Moreover, it can be turned into a self-contained URL, and this is how I use it when I need a draft to quickly jot down notes:

data:text/html,<html contenteditable>

Try to paste this line into the URL input field. If the browser allows you, then you can use Ctrl+B or Ctrl+Ito make the text bold or italic.

We can improve this application a bit by adding styles (yes, I believe that minor typographic improvements important):

data:text/html,<body contenteditable style="line-height:1.5;font-size:20px;">

I added this line to bookmarks and now it is enough to press a few keys to access my weightless editor. You can also use it as a temporary clipboard for pasting text or even images.

It can of course be extended to support different heading, list and indentation styles.

Saving text is done by saving all HTML to a file or by printing it on paper.

Presentations

Several years ago I already wrote simple tool for creating presentationswhich is a standalone HTML file. It can be edited (like markdown text) and it will be rendered as a multi-colored presentation in the style Takahashi

This time as we keep talking contenteditable, let’s write a WYSYWIG slide editor. First, let’s create some blank, editable slides:

<body><script>
for (let i=0; i<50; i++) {
  document.body.innerHTML += `
    <div style="position:relative;width:90%;padding-top:60%;margin:5%;border:1px solid silver;page-break-after:always;">
      <div contenteditable style="outline:none;position:absolute;right:10%;bottom:10%;left:10%;top:10%;font-size:5vmin;">
      </div>
    </div>`;
}
</script>

The number 50 is random, but remember not to use more slides. Every external div Is a slide with a thin stroke. Trick with width and padding-top needed to maintain the aspect ratio of the slide. Try changing the values ​​to see how it affects the structure. Every inner div Is a simple rich text editor with a font large enough to read from a projector screen.

Quite good. But we need titles and lists on our slides, right?

Let’s add hotkeys:

document.querySelectorAll("div>div").forEach(el => {
  el.onkeydown = e=> {
    // `code` will be false if Ctrl or Alt are not pressed
    // `code` will be 0..8 for numeric keys 1..9
    // `code` will be some other numeric value if another key is pressed
    // with Ctrl+Alt hold.
    const code = e.ctrlKey && e.altKey && e.keyCode-49;
    // Find the suitable rich text command, or undefined if the key
    // is out of range
    x = ["formatBlock", "formatBlock", "justifyLeft", "justifyCenter", 
         "justifyRight", "outdent", "indent", "insertUnorderedList"][n];
    // Find command parameter (only for 1 and 2)
    y = ["<h1>", "<div>"][n];
    // Send the command and the parameter (if any) to the editor
    if (x) {
      document.execCommand(x, false, y);
    }
  };
});

Now when you click Ctrl+Alt+1 inside the slide, we’ll make the selected text the title. And if you press Ctrl+Alt+2then it will become normal again. Ctrl+Alt+3Ctrl+Alt+5 change the alignment and 6 and 7 – indent. 8 starts the list. 9 left to your own needs, you can customize this combination. Complete list of operations contenteditable can be found at MDN

By compressing the above code a bit and turning it into a data URL, we get the following slide editor with formatting of about 600 bytes:

data:text/html,<style>@page{size: 6in 8in landscape;}</style><body><script>d=document;for(i=0;i<50;i++)d.body.innerHTML+='<div style="position:relative;width:90%;padding-top:60%;margin:5%;border:1px solid silver;page-break-after:always;"><div contenteditable style="outline:none;position:absolute;right:10%;bottom:10%;left:10%;top:10%;font-size:5vmin;"></div></div>';d.querySelectorAll("div>div").forEach(e=>e.onkeydown=e=>{n=e.ctrlKey&&e.altKey&&e.keyCode-49,x=["formatBlock","formatBlock","justifyLeft","justifyCenter","justifyRight","outdent","indent","insertUnorderedList"][n],y=["<h1>","<div>"][n],x&&document.execCommand(x,!1,y)})</script>

Slides can be exported to PDF by printing to file, and then shown on any computer.

Simple drawing

I once created https://onthesamepage.online for quickly creating draft ideas with other people, but despite its simplicity, it is still more than what we will do in this article.

As an absolute minimum, we can only draw lines to the canvas. We need elements <canvas>, multiple mouse / touch handlers, and a flag to indicate that mouse movements should actually draw a line when the mouse button is pressed.

It’s worth mentioning here that accessing elements with id can be obtained through window[id] or window.id… It is curious that for a long time this was not standardized and was a hack from IE, and now it has become standard

In addition, I moved the handling of the mouse cursor position into separate short functions to use it again for handlers. mousedown and mousemove… Also I dropped the borders of the elements bodyto make the canvas full screen.

The minified code is approximately 400 bytes and only allows drawing with the mouse:

<canvas id="v">
<script>
d=document, // shortcut for document
d.body.style.margin=0, // reset style
f=0, // mouse-down flag
c=v.getContext("2d"), // canvas context
v.width=innerWidth, // make canvas element fullscreen
v.height=innerHeight,
c.lineWidth=2, // make lines a bit wider
x=e=>e.clientX||e.touches[0].clientX, // get X position from mouse/touch
y=e=>e.clientY||e.touches[0].clientY, // get Y position from mouse/touch
d.onmousedown=d.ontouchstart=e=>{f=1,e.preventDefault(),c.moveTo(x(e),y(e)),c.beginPath()},
d.onmousemove=d.ontouchmove=e=>{f&&(c.lineTo(x(e),y(e)),c.stroke())},
d.onmouseup=d.ontouchend=e=>f=0
</script>

And here’s what a one-line bookmark looks like:

data:text/html,<canvas id="v"><script>d=document,d.body.style.margin=0,f=0,c=v.getContext("2d"),v.width=innerWidth,v.height=innerHeight,c.lineWidth=2,x=e=>e.clientX||e.touches[0].clientX,y=e=>e.clientY||e.touches[0].clientY,d.onmousedown=d.ontouchstart=e=>{f=1,e.preventDefault(),c.moveTo(x(e),y(e)),c.beginPath()},d.onmousemove=d.ontouchmove=e=>{f&&(c.lineTo(x(e),y(e)),c.stroke())},d.onmouseup=d.ontouchend=e=>f=0</script>

Spreadsheet editor

This application will probably be the most complex and largest, but we will try not to exceed the 1KB limit per application.

The structure will be simple. There are tables in HTML, so why not use them? Since spreadsheet cells are usually addressed by letter + number, we will limit the table to 26×100 cells. It would be logical to create rows and cells dynamically, in a loop. Simple styling will make your spreadsheet look prettier:

<table id="t">

t.style.borderCollapse="collapse"; // remove gaps between cells
for (let i = 0; i < 101; i++) {
  const row = t.insertRow(-1);
  for (let j = 0; j < 27; j++) {
    // convert column index j to a letter (char code of "A" is 65)
    const letter = String.fromCharCode(65+j-1); // 1=A, 2=B, 3=C etc
    const cell = row.insertCell(-1);
    cell.style.border = "1px solid silver"; // make thin grey border
    cell.style.textAlign = "right";         // right-align, like excel
    if (i > 0 && j > 0) {
      // add identifiable input field, this is where formula is entered
      const field = document.createElement('input');
      field.id = letter + i; // i.e, "B3"
      cell.appendChild(field);
    } else if (i > 0) {
      // Row numbers
      cell.innerHTML = i;
    } else if (j > 0) {
      // Column letters
      cell.innerHTML = letter;
    }
  }
}

We now have a large grid of cells with columns and rows. Next, you need to add an expression evaluator. We can write a dirty, but quite working calculator on three arrays – an array of all input fields (to get their entered values, numbers and formulas), an array with a smart getter that calls eval (), if a variable is required and it is bound by a formula to the input field, and a cache of the last entered values ​​for each field:

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 = () => {
  inputs.map(field => {
    try {
      field.value = D[field.id];
    } catch (e) { /* ignore */}
  });
};

// We also need to customize our field initialization code:
field.onfocus = () => {
  // When element is focused - replace its calculated value with its formula
  field.value = cache[field.id] || "";
};
field.onblur = () => {
  // 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 = () => {
  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})

Now the spreadsheet should work – if we enter, for example, “42” in A1 and “= A1 + 3” in A2, then switching focus from A2 we should see 45.

After carefully minifying the code, we end up with a working spreadsheet of about 800 bytes:

data:text/html,<table id="t"><script>for(I=[],D={},C={},calc=()=>I.forEach(e=>{try{e.value=D[e.id]}catch(e){}}),t.style.borderCollapse="collapse",i=0;i<101;i++)for(r=t.insertRow(-1),j=0;j<27;j++)c=String.fromCharCode(65+j-1),d=r.insertCell(-1),d.style.border="1px solid #ccc",d.style.textAlign="right",d.innerHTML=i?j?"":i:c,i*j&&I.push(d.appendChild((f=>(f.id=c+i,f.style.border="none",f.style.width="4rem",f.style.textAlign="right",f.onfocus=e=>f.value=C[f.id]||"",f.onblur=e=>{C[f.id]=f.value,calc()},get=()=>{let v=C[f.id]||"";if("="!=v.charAt(0))return isNaN(parseFloat(v))?v:parseFloat(v);with(D)return eval(v.substring(1))},Object.defineProperty(D,f.id,{get:get}),Object.defineProperty(D,f.id.toLowerCase(),{get:get}),f))(document.createElement("input"))))</script>

Are you seriously?

Yes, of course, all this cannot be a real replacement for an office suite. However, this is a good example of minimalism and a small amount of code. All of these applications are ephemeral, they lose their state when the page is refreshed, and there seems to be no way to make the data URL persist their state. But they can be useful as handy bookmarks if you need to evaluate a couple of expressions or throw a draft of a note without opening heavy “real” office applications. As a bonus, all of these tiny apps are extremely respectful of privacy and don’t transfer (or store) your data.

So yes, this looks more like a joke than a serious application, however I created a repository for such tiny applications in case anyone wants to use them or customize them to their needs: http://github.com/zserge/awfice… Pull requests and further improvements are welcome!


Advertising

Meet! For the first time in Russia – epic servers!
Powerful VDS based on the latest AMD EPYC processors. Processor frequency up to 3.4 GHz. Maximum configuration – 128 CPU cores, 512 GB RAM, 4000 GB NVMe!

Similar Posts

Leave a Reply Cancel reply