Всем нам знаком традиционный офисный пакет — текстовый редактор, электронные таблицы, программа для создания презентаций, возможно, приложение для создания диаграмм или заметок. Всё это мы видим в Microsoft Office и в Google Docs. Все эти программы мощны и объёмны. Но каким будет минимальное количество кода, требуемое для создания офисного пакета?

Платформа


Очевидно, что наш офисный пакет не будет десктопным приложением с GUI — для создания чего-то подобного требуется много кода и труда. То же самое относится и к нативным мобильным приложениям. Мы можем рассмотреть возможность создания консольного (терминального) приложения, и на самом деле существуют абсурдно маленькие текстовые редакторы или электронные таблицы, но будет гораздо проще, если мы выберем в качестве платформы браузер.

В браузерах уже есть вполне достойный редактор текста с форматированием (contenteditable) и они являются очень хорошими (хоть и небезопасными) вычислителями математических выражений.

Насколько же малым мы сможем сделать пакет?

Текстовый редактор


На самом деле, это «приложение», которым я пользуюсь уже многие годы:

<html contenteditable>

Да, и это всё. Более того, это можно превратить в самодостаточный URL, и именно так я его использую, когда мне требуется черновик, чтобы быстро набросать заметки:

data:text/html,<html contenteditable>

Попробуйте вставить эту строку в поле ввода URL. Если браузер вам позволит, то вы сможете использовать Ctrl+B или Ctrl+I, чтобы делать текст жирным или курсивным.

Мы можем немного улучшить это приложение, добавив стилей (да, я считаю, что небольшие типографические улучшения важны):

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

Я добавил эту строку в закладки и теперь для доступа к моему невесомому редактору достаточно нажать на несколько клавиш. Можно также использовать его как временный буфер обмена для вставки текста или даже изображений.

Разумеется, его можно расширить, чтобы он поддерживал разные стили заголовков, списки и отступы.

Сохранение текста выполняется сохранением всего HTML в файл или печатью его на бумаге.

Презентации


Несколько лет назад я уже написал простой инструмент для создания презентаций, который представляет собой самостоятельный файл HTML. Его можно редактировать (наподобие markdown-текста) и он будет рендериться как разноцветная презентация в стиле Takahashi.

На этот раз, поскольку мы продолжаем говорить contenteditable, давайте напишем WYSYWIG-редактор слайдов. Во-первых, создадим несколько пустых слайдов с возможностью редактирования:

<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>

Число 50 выбрано произвольно, но не забывайте, что большего количества слайдов использовать не стоит. Каждый внешний div — это слайд с тонкой обводкой. Трюк с width и padding-top нужен для сохранения соотношения сторон слайда. Попробуйте изменять значения, чтобы увидеть, как это влияет на структуру. Каждый внутренний div — это простой редактор форматированного текста с достаточно большим шрифтом для чтения с экрана проектора.

Вполне неплохо. Но ведь нам нужны на слайдах заголовки и списки, правда?

Давайте добавим горячие клавиши:

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);
    }
  };
});

Теперь при нажатии Ctrl+Alt+1 внутри слайда мы сделаем выбранный текст заголовком. А если нажать Ctrl+Alt+2, то он снова станет обычным. Ctrl+Alt+3-Ctrl+Alt+5 меняют выравнивание, а 6 и 7 — отступ. 8 начинает список. 9 оставлена под ваши собственные нужды, можете настроить эту комбинацию. Полный список операций contenteditable можно найти на MDN.

Немного ужав показанный выше код и превратив его в data URL, мы получим следующий редактор слайдов с форматированием размером около 600 байт:

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>

Слайды можно экспортировать в PDF при помощи печати в файл, а после этого демонстрировать на любом компьютере.

Простое рисование


Однажды я создал https://onthesamepage.online для быстрого создания черновиков идей совместно с другими людьми, но несмотря на его простоту, он всё равно больше, чем мы сделаем в этой статье.

В качестве абсолютного минимума мы можем только отрисовывать линии на canvas. Нам нужны элементы <canvas>, несколько обработчиков мыши/касаний и флаг, обозначающий, что движения мыши действительно должны рисовать линию при нажатии клавиши мыши.

Здесь стоит упомянуть, что доступ к элементам с id можно получать через window[id] или window.id. Любопытно, что долгое время это не было стандартизировано и являлось хаком из IE, а теперь стало стандартом.

Кроме того, я переместил обработку положения курсора мыши в отдельные короткие функции, чтобы использовать её ещё раз для обработчиков mousedown и mousemove. Также я сбросил границы элементов body, чтобы canvas был полноэкранным.

Минифицированный код занимает примерно 400 байт и позволяет только рисовать мышью:

<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>

А вот как выглядит однострочная закладка:

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>

Редактор электронных таблиц


Наверно, это приложение будет самым сложным и большим, но мы попытаемся не превышать лимит в 1 КБ на приложение.

Структура будет простой. В HTML есть таблицы, так почему бы не использовать их? Так как ячейки электронной таблицы обычно адресуются по «букве» + «числу», то ограничим таблицу размером 26x100 ячеек. Логично будет создавать строки и ячейки динамически, в цикле. Простая стилизация сделает электронную таблицу красивее:

<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;
    }
  }
}

Теперь у нас есть большая сетка из ячеек со столбцами и строками. Далее нужно добавить вычислитель выражений. Мы можем написать грязный, но вполне работающий вычислитель на трёх массивах — массиве всех полей ввода (для получения их введённых значений, чисел и формул), массиве с умным геттером, вызывающим eval(), если требуется переменная и она связана формулой с полем ввода, и кэше последних введённых значений для каждого поля:

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})

Теперь электронная таблица должна работать — если мы введём, например, «42» в A1 и "=A1+3" в A2, то переключении фокуса с A2 мы должны увидеть 45.

После аккуратной минимизации кода мы получаем работающую электронную таблицу размером примерно 800 байт:

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>

Вы серьёзно?


Да, разумеется, всё это никак не может стать настоящей заменой офисного пакета. Однако это хороший пример минимализма и небольшого объёма кода. Все эти приложения эфемерны, они теряют своё состояние, если обновить страницу, и, похоже, никак нельзя сделать так, чтобы data URL сохраняли их состояние. Но они могут быть полезны как удобные закладки, если вам нужно вычислить пару выражений или накидать черновик заметки, не открывая тяжёлые «реальные» офисные приложения. В качестве бонуса: все эти крошечные приложения чрезвычайно уважительно относятся к конфиденциальности и не передают ваши данные (а также не хранят их).

Поэтому да, это больше похоже на шутку, чем на серьёзное приложение, однако я создал репозиторий для таких крошечных приложений на случай, если кому-нибудь захочется их использовать или настроить под свои нужды: http://github.com/zserge/awfice. Приветствуются пулл-реквесты и дальнейшие усовершенствования!



На правах рекламы


Встречайте! Впервые в России — эпичные серверы!
Мощные VDS на базе новейших процессоров AMD EPYC. Частота процессора до 3.4 GHz. Максимальная конфигурация — 128 ядер CPU, 512 ГБ RAM, 4000 ГБ NVMe!

Комментарии (26)