4
\$\begingroup\$

Overview

HTML Editor is an online HTML editor with a minimalist approach. Edit your HTML, CSS, and JavaScript code and monitor the instant live preview. It can also create, open and edit other types of text files such as .txt, .css, .js, .svg, etc.

Please review the source code and provide feedback.

Source code

const run = document.getElementById('run'),
  iframe = document.querySelector('iframe'),
  textarea = document.querySelector('textarea'),
  download = document.getElementById('download'),
  chooseFile = document.getElementById('chooseFile'),
  textareaWrap = document.getElementById('textareaWrap'),
  iframeWrap = document.getElementById('iframeWrap'),
  output = document.querySelector('output'),
  main = document.querySelector('main'),
  footer = document.querySelector('footer'),
  notification = document.getElementById('notification');

function preview() {
  if (run.checked) {
    iframe.replaceWith(iframe); // a fresh iframe to delete JavaScript variables
    const iframeDoc = iframe.contentDocument;
    iframeDoc.write(textarea.value);
    iframeDoc.close();
  }
}

textarea.addEventListener('input', preview);
run.addEventListener('change', preview);

for (const event of ['click', 'contextmenu']) {
  download.addEventListener(event, function() {
    const blob = new Blob([textarea.value], {
      type: 'text/html; charset=utf-8'
    });
    this.href = URL.createObjectURL(blob);
  });
}

document.getElementById('fontSize').addEventListener('change', function() {
  textarea.style.fontSize = this.value + 'px';
});

document.getElementById('reset').addEventListener('click', function() {
  if (!textarea.value || textarea.value != textarea.defaultValue && confirm('Your input will be lost.\nAre you sure you want to reset?')) {
    chooseFile.value = '';
    download.download = 'template.html';
    textarea.value = textarea.defaultValue;
    preview();
  }
});

document.getElementById('select').addEventListener('click', function() {
  textarea.select();
});

chooseFile.addEventListener('change', async function() {
  const file = this.files[0];
  if (file) { // to ensure that there's a file to read so Chrome, for example, doesn't run this function when you cancel choosing a new file
    download.download = file.name;
    textarea.value = await file.text();
    preview();
  }
});

document.getElementById('textareaSize').addEventListener('input', function() {
  const range = this.value;
  textareaWrap.style.flexGrow = range;
  iframeWrap.style.flexGrow = 100 - range;
  output.value = (range / 100).toFixed(2);
});

document.getElementById('viewsToggle').addEventListener('change', function() {
  main.classList.toggle('horizontal');
});

document.getElementById('themesToggle').addEventListener('change', function() {
  textarea.classList.toggle('dark');
});

document.getElementById('footerToggle').addEventListener('click', function() {
  this.classList.toggle('on');
  footer.toggleAttribute('hidden');
});

document.getElementById('copy').addEventListener('click', function() {
  navigator.clipboard.writeText(location);
  function toggleNotification() {
    notification.toggleAttribute('hidden');
  }
  toggleNotification();
  setTimeout(toggleNotification, 1500);
});

window.addEventListener('beforeunload', function(event) {
  if (textarea.value && textarea.value != textarea.defaultValue) {
    event.preventDefault();
    event.returnValue = '';
  }
});

preview();
html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
}

body {
  display: flex;
  flex-direction: column;
}

header,
footer:not([hidden]) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
  padding: 5px;
}

header {
  background: linear-gradient(#FFF, #CCC);
}

label,
#download,
#fontSize,
#reset,
#select,
#chooseFile,
output,
span {
  font: bold 11px Arial;
  color: #333;
}

[type="checkbox"] {
  margin: 0 5px 0 0;
}

[for="fontSize"] {
  margin-left: 5px;
}

#fontSize,
button,
#textareaSize {
  margin: 0;
}

#chooseFile {
  margin: 0 auto 0 0;
}

#textareaSize,
iframe {
  padding: 0;
}

output {
  margin-right: 5px;
  font-family: monospace;
}

#footerToggle {
  width: 16px;
  height: 16px;
  border: 1px solid #666;
  border-bottom-width: 5px;
  padding: 0;
  background: transparent;
}

#footerToggle.on {
  border-color: #333;
  background: #FFF;
}

main {
  flex: 1;
  display: flex;
}

main.horizontal {
  flex-direction: column;
}

div {
  flex: 0px;
  position: relative;
}

#iframeWrap {
  border-left: 5px solid #CCC;
}

main.horizontal #iframeWrap {
  border-left: 0;
  border-top: 5px solid #CCC;
}

div * {
  position: absolute;
  width: 100%;
  height: 100%;
  margin: 0;
  border: 0;
  background: #FFF;
}

textarea {
  box-sizing: border-box;
  padding: 5px;
  outline: 0;
  resize: none;
  font-size: 14px;
  color: #333;
}

textarea.dark {
  background: #333;
  color: #FFF;
}

footer {
  background: linear-gradient(#CCC, #FFF);
}

img {
  display: block;
}

#copy {
  border: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}

address {
  margin-left: auto;
  font: italic 16px 'Times New Roman';
  color: #333;
}

address a {
  color: inherit;
}
<header>
  <label for="run">Run</label>
  <input type="checkbox" id="run" checked>
  <a href="" download="template.html" title="Download the HTML document" id="download">Download</a>
  <label for="fontSize">Font size</label>
  <select id="fontSize">
    <option>12</option>
    <option>13</option>
    <option selected>14</option>
    <option>15</option>
    <option>16</option>
    <option>17</option>
    <option>18</option>
    <option>19</option>
    <option>20</option>
  </select>
  <button type="button" id="reset">Reset</button>
  <button type="button" id="select">Select</button>
  <input type="file" accept="text/html" id="chooseFile">
  <label for="textareaSize">Textarea size</label>
  <input type="range" id="textareaSize">
  <output for="textareaSize">0.50</output>
  <label for="viewsToggle">Horizontal view</label>
  <input type="checkbox" id="viewsToggle">
  <label for="themesToggle">Dark theme</label>
  <input type="checkbox" id="themesToggle">
  <button type="button" title="Toggle footer" id="footerToggle"></button>
</header>
<main>
  <div id="textareaWrap">
    <textarea spellcheck="false"><!doctype html>
<html lang="en">
<head>
  <title>HTML Document Template</title>
  <style>
    p {
      font-family: Arial;
    }
  </style>
</head>
<body>
  <p>Hello, world!</p>
  <script>
    console.log(document.querySelector('p').textContent);
  </script>
</body>
</html></textarea>
  </div>
  <div id="iframeWrap">
    <iframe></iframe>
  </div>
</main>
<footer hidden>
  <span>Share</span>
  <a href="https://twitter.com/intent/tweet?text=HTML%20Editor%3A%20online%20HTML%20editor%20with%20real-time%20preview&url=https%3A%2F%2Fhtmleditor.gitlab.io" target="_blank"><img src="images/x.svg" width="16" height="16" alt="X"></a>
  <a href="https://www.facebook.com/sharer.php?u=https%3A%2F%2Fhtmleditor.gitlab.io&t=HTML%20Editor%3A%20online%20HTML%20editor%20with%20real-time%20preview" target="_blank"><img src="images/facebook.svg" width="16" height="16" alt="Facebook"></a>
  <a href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fhtmleditor.gitlab.io" target="_blank"><img src="images/linkedin.svg" width="16" height="16" alt="LinkedIn"></a>
  <a href="mailto:?subject=HTML%20Editor%3A%20online%20HTML%20editor%20with%20real-time%20preview&body=https%3A%2F%2Fhtmleditor.gitlab.io" target="_blank"><img src="images/email.svg" width="16" height="16" alt="Email"></a>
  <button type="button" id="copy"><img src="images/link.svg" width="16" height="16" alt="Link"></button>
  <span id="notification" hidden>Copied!</span>
  <address><a href="https://codereview.stackexchange.com/questions/291941/html-editor-online-html-editor-with-real-time-preview" title="Code Review Stack Exchange">Feedback</a> | Created by <a href="https://mori.pages.dev" rel="author">Mori</a></address>
</footer>
\$\endgroup\$
2
  • \$\begingroup\$ I usually see id and class selectors as kebab-case instead of camelCase. That's also what the Google HTML/CSS style guide recommends: hyphens for id and hyphens for class \$\endgroup\$
    – tdy
    May 7 at 18:04
  • \$\begingroup\$ @tdy: Thanks for the feedback! While I appreciate the Google HTML/CSS Style Guide's recommendation for kebab-case for IDs and classes, I personally prefer to use camelCase throughout my code for consistency. I find it easier to create and maintain const iframeWrap = document.getElementById('iframeWrap'); compared to const iframeWrap = document.getElementById('iframe-wrap');, for example. \$\endgroup\$
    – Mori
    May 8 at 11:14

1 Answer 1

2
+50
\$\begingroup\$

This file browser:

<input type="file" accept="text/html" id="chooseFile">

inconsistent buttons

Has a couple issues:

  1. Inaccessible label
  2. Inconsistent style

If you don't care about style consistency

Then at least add an accessible label. I realize it shows "Browse..." (depending on the browser), but that text is not readable by screen readers.

Note that the "Reset" and "Select" controls are <button> elements, so they already have text labels. The "Browse" control is an <input> element which doesn't include a label, so that's why we have to provide our own accessible hidden label.

The simplest fix here would be to give it an aria-label:

<input type="file" accept="text/html" id="chooseFile" aria-label="Choose file">
<!--                                                  ^^^^^^^^^^^^^^^^^^^^^^^^ -->

If you do care about style consistency

Then we can address both the accessibility and style at once.

The file chooser element is styled by the browser, so we can't restyle it, but we can hide the file chooser and use a styled button to control it:

consistent buttons

So now the visible element is a <button> instead of <input> and can be styled to match the other buttons. It also has accessible text already, so we don't need to do anything extra for accessibility.

document.getElementById('browse').addEventListener('click', function() {
  chooseFile.click();
});
#chooseFile {
  display: none;
}

#browse {
  font: bold 11px Arial;
  color: #333;
}
<button type="button" id="browse">Browse...</button>
<input type="file" accept="text/html" id="chooseFile">

\$\endgroup\$
2
  • \$\begingroup\$ Regarding the first issue, I just tried some known screen readers, namely Microsoft Narrator, NVDA, and Apple VoiceOver. They all read the browser-generated label as well as the filename. Still, I added aria-label="Choose file" and tested it with NVDA and VoiceOver. The result was that they stopped reading the filename, and they read the browser-generated label in addition to the aria-label value if they are different. In Firefox, for example, you hear both "Choose file" and "Browse." \$\endgroup\$
    – Mori
    May 14 at 8:47
  • \$\begingroup\$ As for the second issue, I do care about consistency when it comes to the same elements, but an <input type="file"> is not basically a <button> element. "we can hide the file chooser and use a styled button to control it" I agree with Shiv Kumar that "because most users stick to their browser of choice, and therefore are probably used to seeing the control in the default rendition, they'd probably get confused if they saw something different." Moreover, your approach requires adding one more element to display the filename for usability. \$\endgroup\$
    – Mori
    May 14 at 9:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.