A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="fr">
  4. <!-- Has to be within the first 1024 bytes, hence before the `title` element
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset="utf-8">
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name="viewport" content="width=device-width,initial-scale=1">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>World smallest office suite (archive) — David Larlet</title>
  13. <meta name="description" content="Publication mise en cache pour en conserver une trace.">
  14. <!-- That good ol' feed, subscribe :). -->
  15. <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
  16. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  17. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
  18. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
  19. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
  20. <link rel="manifest" href="/static/david/icons2/site.webmanifest">
  21. <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
  22. <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
  23. <meta name="msapplication-TileColor" content="#f7f7f7">
  24. <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
  25. <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
  26. <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
  27. <!-- Documented, feel free to shoot an email. -->
  28. <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
  29. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  30. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  31. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  32. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  35. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  36. <script>
  37. function toggleTheme(themeName) {
  38. document.documentElement.classList.toggle(
  39. 'forced-dark',
  40. themeName === 'dark'
  41. )
  42. document.documentElement.classList.toggle(
  43. 'forced-light',
  44. themeName === 'light'
  45. )
  46. }
  47. const selectedTheme = localStorage.getItem('theme')
  48. if (selectedTheme !== 'undefined') {
  49. toggleTheme(selectedTheme)
  50. }
  51. </script>
  52. <meta name="robots" content="noindex, nofollow">
  53. <meta content="origin-when-cross-origin" name="referrer">
  54. <!-- Canonical URL for SEO purposes -->
  55. <link rel="canonical" href="https://zserge.com/posts/awfice/">
  56. <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
  57. <article>
  58. <header>
  59. <h1>World smallest office suite</h1>
  60. </header>
  61. <nav>
  62. <p class="center">
  63. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  64. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  65. </svg> Accueil</a> •
  66. <a href="https://zserge.com/posts/awfice/" title="Lien vers le contenu original">Source originale</a>
  67. </p>
  68. </nav>
  69. <hr>
  70. <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>
  71. <h2 id="platform">platform</h2>
  72. <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>
  73. <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>
  74. <p>Now, how small can we get?</p>
  75. <h2 id="text-editor">text editor</h2>
  76. <p>This is in fact the “app” I have been using for years:</p>
  77. <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>
  78. <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>
  79. </code></pre></div>
  80. <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>
  81. <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>
  82. <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>
  83. </code></pre></div>
  84. <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>
  85. <p>Of course, this can be further extended to support various heading styles, lists or indentation.</p>
  86. <p>You can save your text by saving the whole HTML as a file, or by printing it on paper.</p>
  87. <h2 id="presentations">presentations</h2>
  88. <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>
  89. <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>
  90. <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>
  91. <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>
  92. <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">`
  93. </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;
  94. </span><span class="sb"> &lt;div contenteditable style="outline:none;position:absolute;right:10%;bottom:10%;left:10%;top:10%;font-size:5vmin;"&gt;
  95. </span><span class="sb"> &lt;/div&gt;
  96. </span><span class="sb"> &lt;/div&gt;`</span><span class="p">;</span>
  97. <span class="p">}</span>
  98. <span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
  99. </code></pre></div>
  100. <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>
  101. <p>Good enough. But we want to have headers and lists on our slides, don’t we?</p>
  102. <p>Let’s add some hotkeys:</p>
  103. <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>
  104. <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>
  105. <span class="c1">// `code` will be false if Ctrl or Alt are not pressed
  106. </span><span class="c1"></span> <span class="c1">// `code` will be 0..8 for numeric keys 1..9
  107. </span><span class="c1"></span> <span class="c1">// `code` will be some other numeric value if another key is pressed
  108. </span><span class="c1"></span> <span class="c1">// with Ctrl+Alt hold.
  109. </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>
  110. <span class="c1">// Find the suitable rich text command, or undefined if the key
  111. </span><span class="c1"></span> <span class="c1">// is out of range
  112. </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>
  113. <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>
  114. <span class="c1">// Find command parameter (only for 1 and 2)
  115. </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>
  116. <span class="c1">// Send the command and the parameter (if any) to the editor
  117. </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>
  118. <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>
  119. <span class="p">}</span>
  120. <span class="p">};</span>
  121. <span class="p">});</span>
  122. </code></pre></div>
  123. <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>
  124. <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>
  125. <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>
  126. </code></pre></div>
  127. <p>The slides can be exported to PDF by printing into the file, and from there could be shown on any computer.</p>
  128. <h2 id="quick-drawing">quick drawing</h2>
  129. <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>
  130. <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>
  131. <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>
  132. <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>
  133. <p>The minified code is roughly 400 bytes and allows you to draw with your mouse, nothing more, nothing less:</p>
  134. <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>
  135. <span class="o">&lt;</span><span class="nx">script</span><span class="o">&gt;</span>
  136. <span class="nx">d</span><span class="o">=</span><span class="nb">document</span><span class="p">,</span> <span class="c1">// shortcut for document
  137. </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
  138. </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
  139. </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
  140. </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
  141. </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>
  142. <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
  143. </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
  144. </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
  145. </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>
  146. <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>
  147. <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>
  148. <span class="o">&lt;</span><span class="err">/script&gt;</span>
  149. </code></pre></div>
  150. <p>Or, as a short one-liner bookmark:</p>
  151. <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>
  152. </code></pre></div>
  153. <h2 id="spreadsheet">spreadsheet</h2>
  154. <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>
  155. <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>
  156. <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>
  157. <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
  158. </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>
  159. <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>
  160. <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>
  161. <span class="c1">// convert column index j to a letter (char code of "A" is 65)
  162. </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
  163. </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>
  164. <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
  165. </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
  166. </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>
  167. <span class="c1">// add identifiable input field, this is where formula is entered
  168. </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>
  169. <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"
  170. </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>
  171. <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>
  172. <span class="c1">// Row numbers
  173. </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>
  174. <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>
  175. <span class="c1">// Column letters
  176. </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>
  177. <span class="p">}</span>
  178. <span class="p">}</span>
  179. <span class="p">}</span>
  180. </code></pre></div>
  181. <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>
  182. <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
  183. data = {}; // smart data accessing object
  184. cache = {}; // cache
  185. // Re-calculate all fields
  186. const calc = () =&gt; {
  187. inputs.map(field =&gt; {
  188. try {
  189. field.value = D[field.id];
  190. } catch (e) { /* ignore */}
  191. });
  192. };
  193. // We also need to customize our field initialization code:
  194. field.onfocus = () =&gt; {
  195. // When element is focused - replace its calculated value with its formula
  196. field.value = cache[field.id] || "";
  197. };
  198. field.onblur = () =&gt; {
  199. // When element loses focus - put formula in cache, and re-calculate everything
  200. cache[field.id] = field.value;
  201. calc();
  202. };
  203. // Smart getter for a field, evaluates formula if needed
  204. const get = () =&gt; {
  205. let value = cache[field.id] || "";
  206. if(value.chatAt(0) == "=") {
  207. // evaluate the formula using "with" hack:
  208. with(data) return eval(value.substring(1));
  209. } else {
  210. // return value as it is, convert to number if possible:
  211. return isNaN(parseFloat(value)) ? value : parseFloat(value);
  212. }
  213. };
  214. // Add smart getter to the data array for both upper and lower case variants:
  215. Object.defineProperty(data, field.id, {get}),
  216. Object.defineProperty(data, field.id.toLowerCase(), {get})
  217. </code></pre></div>
  218. <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>
  219. <p>After carefully minizing the code above, we get the following ~800 byte working spreadsheet:</p>
  220. <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>
  221. </code></pre></div>
  222. <h2 id="are-you-serious">are you serious?</h2>
  223. <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>
  224. <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>
  225. <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>
  226. <p class="date"><em>Oct 11, 2020</em></p>
  227. </article>
  228. <hr>
  229. <footer>
  230. <p>
  231. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  232. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  233. </svg> Accueil</a> •
  234. <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
  235. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
  236. </svg> Suivre</a> •
  237. <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
  238. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
  239. </svg> Pro</a> •
  240. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
  241. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
  242. </svg> Email</a> •
  243. <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
  244. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
  245. </svg> Légal</abbr>
  246. </p>
  247. <template id="theme-selector">
  248. <form>
  249. <fieldset>
  250. <legend><svg class="icon icon-brightness-contrast">
  251. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
  252. </svg> Thème</legend>
  253. <label>
  254. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  255. </label>
  256. <label>
  257. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  258. </label>
  259. <label>
  260. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  261. </label>
  262. </fieldset>
  263. </form>
  264. </template>
  265. </footer>
  266. <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
  267. <script>
  268. function loadThemeForm(templateName) {
  269. const themeSelectorTemplate = document.querySelector(templateName)
  270. const form = themeSelectorTemplate.content.firstElementChild
  271. themeSelectorTemplate.replaceWith(form)
  272. form.addEventListener('change', (e) => {
  273. const chosenColorScheme = e.target.value
  274. localStorage.setItem('theme', chosenColorScheme)
  275. toggleTheme(chosenColorScheme)
  276. })
  277. const selectedTheme = localStorage.getItem('theme')
  278. if (selectedTheme && selectedTheme !== 'undefined') {
  279. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  280. }
  281. }
  282. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  283. window.addEventListener('load', () => {
  284. let hasDarkRules = false
  285. for (const styleSheet of Array.from(document.styleSheets)) {
  286. let mediaRules = []
  287. for (const cssRule of styleSheet.cssRules) {
  288. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  289. continue
  290. }
  291. // WARNING: Safari does not have/supports `conditionText`.
  292. if (cssRule.conditionText) {
  293. if (cssRule.conditionText !== prefersColorSchemeDark) {
  294. continue
  295. }
  296. } else {
  297. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  298. continue
  299. }
  300. }
  301. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  302. }
  303. // WARNING: do not try to insert a Rule to a styleSheet you are
  304. // currently iterating on, otherwise the browser will be stuck
  305. // in a infinite loop…
  306. for (const mediaRule of mediaRules) {
  307. styleSheet.insertRule(mediaRule.cssText)
  308. hasDarkRules = true
  309. }
  310. }
  311. if (hasDarkRules) {
  312. loadThemeForm('#theme-selector')
  313. }
  314. })
  315. </script>
  316. </body>
  317. </html>