0
\$\begingroup\$

Overview

HTML Editor is a web-based HTML editor designed for web developers, designers, and learners. It offers a light-weight, 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.

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

0

Your Answer

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

Browse other questions tagged or ask your own question.