2
\$\begingroup\$

Overview

HTML Editor is a web-based HTML editor designed for web developers, designers, and learners. It offers a lightweight, minimalist environment for writing and previewing HTML, CSS, and JavaScript code in real-time. The editor also supports creating, opening, and editing various text-based file types, such as .txt, .css, .js, .svg, and more.

This project is hosted on GitLab and licensed under the MIT License.

Specific review requests

  • Best practices: How well does my code adhere to best practices and coding conventions?
  • Code structure and readability: Are there any improvements I can make to the organization and clarity of my code?
  • Performance: Are there any optimizations that can enhance the performance of the editor?
  • Features and usability: Do you have any suggestions for additional features or improvements to the existing ones?

Source code

const previewer = document.querySelector('iframe'),
  editor = document.querySelector('textarea'),
  previewSelector = document.getElementById('previewSelector'),
  downloadLink = document.getElementById('downloadLink'),
  fontSizeSelector = document.getElementById('fontSizeSelector'),
  runButton = document.getElementById('runButton'),
  fileInput = document.getElementById('fileInput'),
  editorSizeInput = document.getElementById('editorSizeInput'),
  editorWrapper = document.getElementById('editorWrapper'),
  previewerWrapper = document.getElementById('previewerWrapper'),
  editorSizeOutput = document.querySelector('output'),
  verticalViewToggle = document.getElementById('verticalViewToggle'),
  mainContainer = document.querySelector('main'),
  darkEditorToggle = document.getElementById('darkEditorToggle'),
  spellcheckToggle = document.getElementById('spellcheckToggle'),
  footerContainerToggle = document.getElementById('footerContainerToggle'),
  footerContainer = document.querySelector('footer'),
  copyNotification = document.getElementById('copyNotification');
let previewTimeout = null;

function toggleClass(element, className, force) {
  element.classList.toggle(className, force);
}

function preview() {
  previewer.replaceWith(previewer); // a fresh iframe to delete JavaScript variables
  const previewerDocument = previewer.contentDocument;
  previewerDocument.write(editor.value);
  previewerDocument.close();
}

function timedPreview() {
  const previewSelectorValue = previewSelector.value;
  if (previewSelectorValue === 'Instant') {
    preview();
  } else if (previewSelectorValue !== 'Manual') {
    clearTimeout(previewTimeout);
    previewTimeout = setTimeout(preview, previewSelectorValue);
  }
}

function createDownloadURL() {
  const blob = new Blob([editor.value], {
    type: 'text/html; charset=utf-8'
  });
  downloadLink.href = URL.createObjectURL(blob);
}

function resizeFont() {
  editor.style.fontSize = fontSizeSelector.value + 'px';
}

function toggleRunButton() {
  toggleClass(runButton, 'hidden', previewSelector.value !== 'Manual');
}

function resizeEditor() {
  const editorSizeInputValue = editorSizeInput.value;
  editorWrapper.style.flexGrow = editorSizeInputValue;
  previewerWrapper.style.flexGrow = 100 - editorSizeInputValue;
  editorSizeOutput.value = (editorSizeInputValue / 100).toFixed(2);
}

runButton.addEventListener('click', preview);

editor.addEventListener('input', timedPreview);

downloadLink.addEventListener('click', createDownloadURL);
downloadLink.addEventListener('contextmenu', createDownloadURL);

fontSizeSelector.addEventListener('change', resizeFont);

previewSelector.addEventListener('change', toggleRunButton);

editorSizeInput.addEventListener('input', resizeEditor);

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

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

fileInput.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
    downloadLink.download = file.name;
    editor.value = await file.text();
    timedPreview();
  }
});

verticalViewToggle.addEventListener('click', function() {
  toggleClass(mainContainer, 'vertical');
  toggleClass(this, 'active');
});

darkEditorToggle.addEventListener('click', function() {
  toggleClass(editor, 'dark');
  toggleClass(this, 'active');
});

spellcheckToggle.addEventListener('click', function() {
  editor.spellcheck = !editor.spellcheck;
  toggleClass(this, 'active');
});

footerContainerToggle.addEventListener('click', function() {
  toggleClass(footerContainer, 'hidden');
  toggleClass(this, 'active');
});

document.getElementById('copyButton').addEventListener('click', function() {
  navigator.clipboard.writeText(location);
  toggleClass(copyNotification, 'hidden');
  setTimeout(toggleClass, 2000, copyNotification, 'hidden');
});

// Save the current state to localStorage.
document.addEventListener('visibilitychange', function() {
  if (document.hidden) {
    const currentState = {
      spellcheckToggle: spellcheckToggle.className,
      verticalViewToggle: verticalViewToggle.className,
      darkEditorToggle: darkEditorToggle.className,
      footerContainerToggle: footerContainerToggle.className,
      previewSelector: previewSelector.value,
      fontSizeSelector: fontSizeSelector.value,
      editorSizeInput: editorSizeInput.value,
      editor: editor.value,
    };
    localStorage.setItem('state', JSON.stringify(currentState));
  }
});

// Restore the state from localStorage.
const restoredState = JSON.parse(localStorage.getItem('state'));
if (restoredState) {
  spellcheckToggle.className = restoredState.spellcheckToggle;
  verticalViewToggle.className = restoredState.verticalViewToggle;
  darkEditorToggle.className = restoredState.darkEditorToggle;
  footerContainerToggle.className = restoredState.footerContainerToggle;
  previewSelector.value = restoredState.previewSelector;
  fontSizeSelector.value = restoredState.fontSizeSelector;
  editorSizeInput.value = restoredState.editorSizeInput;
  editor.value = restoredState.editor;
}

// Update the UI based on the restored state.
editor.spellcheck = spellcheckToggle.classList.contains('active');
toggleClass(mainContainer, 'vertical', verticalViewToggle.classList.contains('active'));
toggleClass(editor, 'dark', darkEditorToggle.classList.contains('active'));
toggleClass(footerContainer, 'hidden', !footerContainerToggle.classList.contains('active'));
toggleRunButton();
resizeFont();
resizeEditor();
timedPreview();
html,
body {
  height: 100%;
  padding: 0;
  margin: 0;
}

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

header,
footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
  padding: 5px;
}

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

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

main {
  display: flex;
  flex: 1;
}

main.vertical {
  flex-direction: column;
}

header *,
footer * {
  font: 0.75rem Arial;
  color: #333;
}

select,
button,
input {
  margin: 0;
}

label[for="editorSizeInput"],
address {
  margin-left: auto;
}

#editorSizeInput,
iframe {
  padding: 0;
}

output {
  font-family: monospace;
}

.toggle {
  padding: 2px 6px;
  border: 1px solid #666;
  border-radius: 12px;
  background: transparent;
}

#footerContainerToggle {
  width: 16px;
  height: 16px;
  padding: 0;
  border-bottom-width: 5px;
  border-radius: 0;
}

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

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

img {
  display: block;
}

div {
  position: relative;
}

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

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

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

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

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

.hidden {
  display: none;
}
<header>
  <a href="" download="template.html" id="downloadLink" title="Download HTML document">Download</a>
  <label for="fontSizeSelector">Font size</label>
  <select id="fontSizeSelector">
    <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>
  <label for="previewSelector">Preview</label>
  <select id="previewSelector">
    <option>Instant</option>
    <optgroup label="Delayed">
      <option value="500" selected>0.5 s</option>
      <option value="1000">1 s</option>
      <option value="1500">1.5 s</option>
      <option value="2000">2 s</option>
    </optgroup>
    <option>Manual</option>
  </select>
  <button type="button" id="runButton">Run</button>
  <button type="button" id="resetButton">Reset</button>
  <button type="button" id="selectButton">Select</button>
  <input type="file" accept="text/html" id="fileInput">
  <label for="editorSizeInput">Editor size</label>
  <input type="range" id="editorSizeInput">
  <output for="editorSizeInput"></output>
  <button type="button" class="toggle" id="verticalViewToggle">Vertical view</button>
  <button type="button" class="toggle" id="darkEditorToggle">Dark editor</button>
  <button type="button" class="toggle" id="spellcheckToggle">Spellcheck</button>
  <button type="button" class="toggle" id="footerContainerToggle" title="Toggle footer"></button>
</header>
<main>
  <div id="editorWrapper">
    <textarea><!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="previewerWrapper">
    <iframe></iframe>
  </div>
</main>
<footer>
  <span>Share</span>
  <a href="https://x.com/intent/post?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="copyButton"><img src="images/link.svg" width="16" height="16" alt="Link"></button>
  <span id="copyNotification" class="hidden">Copied!</span>
  <address><a href="https://codereview.stackexchange.com/questions/293330/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\$

1 Answer 1

1
\$\begingroup\$

missing functions

// Save the current state to localStorage.
document.addEventListener( ...

// Restore the state from localStorage.
const restoredState = JSON.parse( ...

// Update the UI based on the restored state.
editor.spellcheck = ...

These are valuable, helpful // comments, and I thank you for them.

But where are the associated helper functions? Define them! You clearly have identified some relevant chunks of work from the Business domain. Name them, for the benefit of future maintenance engineers. Writing code is all about grouping low level details into higher and higher levels of abstraction, until finally we can invoke a single function which accomplishes the business goal, such as launching an editor. Use the tools the language provides you with to clearly express the abstractions you're creating.

resize behavior

Think about someone repeatedly hitting CMD-+ to increase font size, perhaps because they are presenting to a group including folks seated in the back row.

.toggle {
  ...
  border-radius: 12px;
  ...

#footerContainerToggle {
  width: 16px;
  height: 16px;
  ...

It's unclear why we're shooting for "pixel perfect" layout which exactly matches a photoshop mockup image, instead of adjusting to current font size with em units.

nice names

The named DOM elements you create early on, up through copyNotification, are very nice, thank you. They definitely improve the readability of the rest of the code.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the feedback! "But where are the associated helper functions?" Named or helper functions are indeed valuable for reusability and reducing repetition. However, in this case, the three code blocks in question are only executed once: the first is an anonymous function triggered when the page unloads, and the other two are executed during the initial page load. For this reason, I placed them directly in the global scope to keep the code concise and avoid unnecessary abstraction. \$\endgroup\$
    – Mori
    Commented Nov 22 at 8:49
  • \$\begingroup\$ "Think about someone repeatedly hitting CMD-+" Done! \$\endgroup\$
    – Mori
    Commented Nov 26 at 10:22

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.