4
\$\begingroup\$

Note

Please review the new question and ignore the following.


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 iframe = document.querySelector('iframe'),
  textarea = document.querySelector('textarea'),
  previewSettings = document.getElementById('previewSettings'),
  download = document.getElementById('download'),
  fontSize = document.getElementById('fontSize'),
  run = document.getElementById('run'),
  chooseFile = document.getElementById('chooseFile'),
  textareaSize = document.getElementById('textareaSize'),
  textareaWrap = document.getElementById('textareaWrap'),
  iframeWrap = document.getElementById('iframeWrap'),
  output = document.querySelector('output'),
  horizontalView = document.getElementById('horizontalView'),
  main = document.querySelector('main'),
  darkTextarea = document.getElementById('darkTextarea'),
  spellcheck = document.getElementById('spellcheck'),
  footerToggle = document.getElementById('footerToggle'),
  footer = document.querySelector('footer'),
  notification = document.getElementById('notification');
let timeout = null;

function toggleHidden(element, force) {
  element.toggleAttribute('hidden', force);
}

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

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

function timedPreview() {
  const delay = previewSettings.value;
  if (delay == 'Instant') {
    preview();
  } else if (delay != 'Manual') {
    clearTimeout(timeout);
    timeout = setTimeout(preview, delay);
  }
}

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

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

function setPreview() {
  toggleHidden(run, previewSettings.value != 'Manual');
}

function resizeTextarea() {
  const range = textareaSize.value;
  textareaWrap.style.flexGrow = range;
  iframeWrap.style.flexGrow = 100 - range;
  output.value = (range / 100).toFixed(2);
}

run.addEventListener('click', preview);

textarea.addEventListener('input', timedPreview);

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

fontSize.addEventListener('change', resizeFont);

previewSettings.addEventListener('change', setPreview);

textareaSize.addEventListener('input', resizeTextarea);

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

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

horizontalView.addEventListener('click', function() {
  toggleClass(main, 'horizontal');
  toggleClass(this, 'active');
});

darkTextarea.addEventListener('click', function() {
  toggleClass(textarea, 'dark');
  toggleClass(this, 'active');
});

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

footerToggle.addEventListener('click', function() {
  toggleHidden(footer);
  toggleClass(this, 'active');
});

document.getElementById('copy').addEventListener('click', function() {
  navigator.clipboard.writeText(location);
  toggleHidden(notification);
  setTimeout(toggleHidden, 2000, notification);
});

// Save the current state to localStorage.
document.addEventListener('visibilitychange', function() {
  if (document.hidden) {
    const state = {
      fontSize: fontSize.value,
      previewSettings: previewSettings.value,
      textareaSize: textareaSize.value,
      horizontalView: horizontalView.className,
      darkTextarea: darkTextarea.className,
      spellcheck: spellcheck.className,
      footerToggle: footerToggle.className,
      textarea: textarea.value,
    };
    localStorage.setItem('pageState', JSON.stringify(state));
  }
});

// Restore the state from localStorage.
const savedState = JSON.parse(localStorage.getItem('pageState'));
if (savedState) {
  fontSize.value = savedState.fontSize;
  previewSettings.value = savedState.previewSettings;
  textareaSize.value = savedState.textareaSize;
  horizontalView.className = savedState.horizontalView;
  darkTextarea.className = savedState.darkTextarea;
  spellcheck.className = savedState.spellcheck;
  footerToggle.className = savedState.footerToggle;
  textarea.value = savedState.textarea;
}

// Update the UI.
toggleClass(main, 'horizontal', horizontalView.classList.contains('active'));
toggleClass(textarea, 'dark', darkTextarea.classList.contains('active'));
textarea.spellcheck = spellcheck.classList.contains('active');
toggleHidden(footer, !footerToggle.classList.contains('active'));
resizeFont();
setPreview();
resizeTextarea();
timedPreview();
html,
body {
  height: 100%;
  padding: 0;
  margin: 0;
}

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

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

main {
  display: flex;
  flex: 1;
}

main.horizontal {
  flex-direction: column;
}

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

select,
button,
input {
  margin: 0;
}

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

#textareaSize,
iframe {
  padding: 0;
}

output {
  font-family: monospace;
}

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

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

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

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

img {
  display: block;
}

div {
  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%;
  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;
}
<header>
  <a href="" download="template.html" title="Download 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>
  <label for="previewSettings">Preview</label>
  <select id="previewSettings">
    <option>Instant</option>
    <optgroup label="Delayed">
      <option value="250">250ms</option>
      <option value="500" selected>500ms</option>
      <option value="750">750ms</option>
      <option value="1000">1000ms</option>
    </optgroup>
    <option>Manual</option>
  </select>
  <button type="button" id="run">Run</button>
  <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"></output>
  <button type="button" class="toggle" id="horizontalView">Horizontal view</button>
  <button type="button" class="toggle" id="darkTextarea">Dark textarea</button>
  <button type="button" class="toggle" id="spellcheck">Spellcheck</button>
  <button type="button" title="Toggle footer" class="toggle" id="footerToggle"></button>
</header>
<main>
  <div id="textareaWrap">
    <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="iframeWrap">
    <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="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/292588/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

6
\$\begingroup\$

First of all, a little disclaimer: I'm not a professional Web frontend developer, every single line of HTML, CSS and JavaScript that I've written was for personal projects. The following is my personal opinion, not necessarily professional advice.

HTML

Use CSS for styling

While you mostly do use CSS for styling, the width and height of the social media links are coded in HTML.

One HTML element per line

You define each social media link in a single line, although they consist of multiple elements (a, img, ...). Please split them up into multiple lines, such that each line contains a single element:

<a href="...">
    <img src="..."/>
</a>

Regardless of programming language, avoid getting lines too busy.

Naming

Do your future self a favor and choose semantically meaningful names for your objects. For example, "textareaWrap" and "iframeWrap" tell me nothing about what they are on the site other than wrappers for some HTML elements. Semantically meaningful names would instead be, for example, "texteditor" or "preview-window".

Also, please try to be specific in the role the elements play for your application. For example, you (mis-)use the <header> tag as a container for a toolbar. The role of a header is to provide a header section for a document, usually containing things like title, logo, general info or navigation. Why not be explicit and use <header class="toolbar"> instead? (And a div would semantically be more appropriate imho)

Templates

Your social media links are a good fit for using HTML's <template> tag. This tag allows you to define partial document trees that you can instantiate and insert into different parts of your website with JavaScript. If you were to change the structure of your social media links in the future, this leaves you a single place to change them instead of four.

Default text of <textarea>

The default text of the <textarea> element is HTML text itself, which I find a little confusing. Also, it's indentation is a little odd. Maybe there are better solutions for this?

iframe

As far as I know, using iframes is rather discouraged in modern web development, but there are definitely good use-cases for using them. You use the iframe element basically as a renderer for the file content. You could achieve the same by simply

view.innerHTML = content;

(although this method is admittedly also not very elegant).

CSS

Favor classes over IDs

Professional web devs usually use classes over IDs for styling. IDs must be unique on the site (although no one enforces this), while multiple objects can share the same class. Hence, classes generally allow more flexibility. For example, I think a class would be more appropriate for #iframeWrap because it has more the characteristic of a widget rather than a specific unique element in your app. For a discussion on IDs vs. classes, see css-tricks.com.

Favor classes over styling HTML elements

When specifically styling the input window of your editor, you better use a semantically meaningful CSS class as a target, such as .content-field instead of a generic textarea. This allows you to extend your application easier in the future. Try to be as specific as possible and only style HTML elements if under any circumstance, each element has to have the same style (such as h2 in a text document).

Grouping of styles

Is there any specific logic to the order of styles in your CSS? In my own projects, I usually group them by element class (e.g., widget) and set the different groups visually apart with comments, e.g.,

/*
 * Toolbar
 */

select,
button,
input {
  margin: 0;
}

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

...


/*
 * Input field
 */

textarea {
  ...
}

textarea.dark {
  ...
}

CSS variables

Did you know that CSS now supports variables? Very useful, e.g., for color palettes:

body {
    --text-bg-color: #fff;
    --text-color: #000;
    --accent-color: #f00;
    --widget-bg-color: #ccc;
}

body.dark {
    --text-bg-color: #333;
    --text-color: #fff;
    --accent-color: #f00;
    --widget-bg-color: #666;
}

These variables would then be available in every child element of the <body> tag, such as:

textarea {
    color: var(--text-color);
    background-color: var(--text-bg-color);
}

JavaScript

Reference specific elements

As above, in terms of maintainability, you're probably better off being as specific as possible in your references, e.g.,

const textarea = document.querySelector('textarea');

should probably be

const textarea = document.querySelector('.content-field');

(By the way, where is this darkTextarea element?)

Naming

As above, be specific in your names. The variable textarea tells me nothing about what this said text area is, what it role plays, what it should do. It just tells me what it IS, which might not be relevant at all (e.g., another HTML tag might be more suitable for this role?). It's an implementation detail codified in a name, it lacks semantic information.

Always use === or !==

Did you know that document.querySelector("body") == "[object HTMLBodyElement]" evaluates to true? Double (in)equality will try to convert the arguments to comparable types, so strange things may happen. Unless you specifically want this behavior, you should use the triple (in) equality operators === and !== that compare the values as-is

Strict equality is almost always the correct comparison operation to use.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness

Coupling

All of your JavaScript code is tightly coupled to the elements in your HTML, each method intransparently and directly manipulates the global state. At this scale, this is largely ok, but it can quickly become unmanageable when growing and developing the application further. It quickly becomes intransparent who changes what when and why. Try to properly encapsulate components of your code and let them communicate over well-defined interfaces. Let them manage only their internal state and avoid global state as much as possible.

For example, you could add a ContentField which is instantiated with a textarea object and provides methods to query the current content, toggle spellcheck, etc.

Besides, any idea on how you would test this? Thinking about how to automatically test your code (i.e., unit tests) will help you to naturally write loosely coupled code.

Code injection

Never blindly render user input. Please also consider the possibility of code injection when rendering the file contents. What if someone opens a file with malicious JavaScript code inside?

Other notes

Web Components

Why not wrapping this up in a web component? That ensures modularity and maintainability by wrapping your text editor related code in one single place/component and sets it apart from other components, such as logins, settings, etc.

Performance

Performance: Are there any optimizations that can enhance the performance of the editor?

Why? Is it too slow? Does it feel sluggish? Does your target audience use PCs from the 80s? Your application is extremely simple, I doubt that any optimization will be measurable.

Features

Features and usability: Do you have any suggestions for additional features or improvements to the existing ones?

There are so many features for a text editor. What about syntax highlighting? Shortcuts? Multi-line edits? But I think, this is out of scope for this platform.

\$\endgroup\$
14
  • 1
    \$\begingroup\$ Thanks! "While you mostly do use CSS for styling, the width and height of the social media links are coded in HTML." While it's generally good practice to use CSS for styling, including width and height in the HTML for images helps the browser calculate the aspect ratio before loading. This prevents layout shifts and is recommended by MDN and Smashing Magazine. \$\endgroup\$
    – Mori
    Commented Jul 16 at 19:48
  • \$\begingroup\$ "You define each social media link in a single line, although they consist of multiple elements (a, img, ...)." There are no strict rules for this, and I see both single-line and multi-line versions in the HTML Standard. It ultimately comes down to team conventions and personal preference. I prefer the current version for its conciseness and reduced markup clutter. "Do your future self a favor and choose semantically meaningful names for your objects." You raised a valid point. I will try to find more descriptive and semantic names in my code. \$\endgroup\$
    – Mori
    Commented Jul 16 at 19:49
  • \$\begingroup\$ "you (mis-)use the <header> tag as a container for a toolbar" According to MDN, "The <header> HTML element represents introductory content, typically a group of introductory or navigational aids. It may contain some heading elements but also a logo, a search form, an author name, and other elements." In my case, it is used to contain the controls for the HTML editor. This is an appropriate use of the header element as it encapsulates content that introduces and provides functionality for the page. \$\endgroup\$
    – Mori
    Commented Jul 16 at 19:49
  • 1
    \$\begingroup\$ "More specific code is usually more robust than lax code with implicit, undocumented assumptions." Convinced. Done! 🙏 \$\endgroup\$
    – Mori
    Commented Jul 19 at 16:18
  • 1
    \$\begingroup\$ "Do your future self a favor and choose semantically meaningful names for your objects." Done! 🙏 \$\endgroup\$
    – Mori
    Commented Aug 16 at 16:20

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.