7
\$\begingroup\$

Note

Please review the new question and ignore the following.


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

var runner = document.getElementById('runner'),
  editor = document.getElementById('editor'),
  downloader = document.getElementById('downloader'),
  fileChooser = document.getElementById('fileChooser');

function preview() {
  if (runner.checked) {
    document.getElementById('viewer').srcdoc = editor.value;
  }
}

editor.addEventListener('input', preview);
runner.addEventListener('change', preview);

['click', 'contextmenu'].forEach(event => downloader.addEventListener(event, function() {
  var blob = new Blob([editor.value], {type: 'text/html'});
  this.href = URL.createObjectURL(blob);
}));

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

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

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

fileChooser.addEventListener('change', async function() {
  var 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
    downloader.download = file.name;
    editor.value = await file.text();
    preview();
  }
});

document.getElementById('resizer').addEventListener('input', function() {
  var resizerVal = this.value;
  document.getElementById('editorWrapper').style.flexGrow = resizerVal;
  document.getElementById('viewerWrapper').style.flexGrow = 100 - resizerVal;
  document.getElementById('indicator').value = (resizerVal / 100).toFixed(2);
});

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

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

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

document.getElementById('copier').addEventListener('click', function() {
  navigator.clipboard.writeText('https://htmleditor.gitlab.io');
  function toggleNotification() {
    document.getElementById('notification').toggleAttribute('hidden');
  }
  toggleNotification();
  setTimeout(toggleNotification, 1500);
});

window.addEventListener('beforeunload', function(event) {
  if (editor.value && editor.value != editor.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,
#downloader,
select,
#resetter,
#selector,
#fileChooser,
output,
span {
  font: bold 11px Arial;
  color: #333;
}

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

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

select,
button,
#resizer {
  margin: 0;
}

#fileChooser {
  margin: 0 auto 0 0;
}

#resizer,
iframe {
  padding: 0;
}

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

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

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

main {
  flex: 1;
  display: flex;
}

main.horizontal {
  flex-direction: column;
}

div {
  flex: 0px;
  min-width: 0;
  min-height: 0;
}

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

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

div * {
  display: block;
  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;
}

#copier {
  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="runner">Run</label>
  <input type="checkbox" id="runner" checked>
  <a href="" download="template.html" title="Download the HTML document" id="downloader">Download</a>
  <label for="fontSizer">Font size</label>
  <select id="fontSizer">
    <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="resetter">Reset</button>
  <button type="button" id="selector">Select</button>
  <input type="file" accept="text/html" id="fileChooser">
  <label for="resizer">Editor size</label>
  <input type="range" id="resizer">
  <output for="resizer" id="indicator">0.50</output>
  <label for="viewsToggler">Horizontal view</label>
  <input type="checkbox" id="viewsToggler">
  <label for="themesToggler">Dark theme</label>
  <input type="checkbox" id="themesToggler">
  <button type="button" title="Toggle footer" id="footerToggler"></button>
</header>
<main id="main">
  <div id="editorWrapper">
    <textarea spellcheck="false" id="editor"><!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="viewerWrapper">
    <iframe id="viewer"></iframe>
  </div>
</main>
<footer id="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/twitter.svg" width="16" height="16" alt="Twitter"></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="copier"><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/284918/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\$

4 Answers 4

4
+150
\$\begingroup\$

I'll be reviewing the code in your GitLab repository as it has this code in one file:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Edit your HTML, CSS, and JavaScript code and monitor the instant live preview.">
  <title>HTML Editor: online HTML editor with real-time preview</title>
  <link rel="icon" href="favicon.ico">
  <style>
    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,
    #downloader,
    select,
    #resetter,
    #selector,
    #fileChooser,
    output,
    span {
      font: bold 11px Arial;
      color: #333;
    }

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

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

    select,
    button,
    #resizer {
      margin: 0;
    }

    #fileChooser {
      margin: 0 auto 0 0;
    }

    #resizer,
    iframe {
      padding: 0;
    }

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

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

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

    main {
      flex: 1;
      display: flex;
    }

    main.horizontal {
      flex-direction: column;
    }

    div {
      flex: 0px;
      min-width: 0;
      min-height: 0;
    }

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

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

    div * {
      display: block;
      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;
    }

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

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

    address a {
      color: inherit;
    }
  </style>
</head>

<body>
  <header>
    <label for="runner">Run</label>
    <input type="checkbox" id="runner" checked>
    <a href="" download="template.html" title="Download the HTML document" id="downloader">Download</a>
    <label for="fontSizer">Font size</label>
    <select id="fontSizer">
      <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="resetter">Reset</button>
    <button type="button" id="selector">Select</button>
    <input type="file" accept="text/html" id="fileChooser">
    <label for="resizer">Editor size</label>
    <input type="range" id="resizer">
    <output for="resizer" id="indicator">0.50</output>
    <label for="viewsToggler">Horizontal view</label>
    <input type="checkbox" id="viewsToggler">
    <label for="themesToggler">Dark theme</label>
    <input type="checkbox" id="themesToggler">
    <button type="button" title="Toggle footer" id="footerToggler"></button>
  </header>
  <main id="main">
    <div id="editorWrapper">
      <textarea spellcheck="false" id="editor"><!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="viewerWrapper">
      <iframe id="viewer"></iframe>
    </div>
  </main>
  <footer id="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/twitter.svg" width="16" height="16" alt="Twitter"></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="copier"><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/284918/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>
  <script>
    var runner = document.getElementById('runner'),
      editor = document.getElementById('editor'),
      downloader = document.getElementById('downloader'),
      fileChooser = document.getElementById('fileChooser');

    function preview() {
      if (runner.checked) {
        document.getElementById('viewer').srcdoc = editor.value;
      }
    }

    editor.addEventListener('input', preview);
    runner.addEventListener('change', preview);

    ['click', 'contextmenu'].forEach(event => downloader.addEventListener(event, function() {
      var blob = new Blob([editor.value], {type: 'text/html'});
      this.href = URL.createObjectURL(blob);
    }));

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

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

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

    fileChooser.addEventListener('change', async function() {
      var 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
        downloader.download = file.name;
        editor.value = await file.text();
        preview();
      }
    });

    document.getElementById('resizer').addEventListener('input', function() {
      var resizerVal = this.value;
      document.getElementById('editorWrapper').style.flexGrow = resizerVal;
      document.getElementById('viewerWrapper').style.flexGrow = 100 - resizerVal;
      document.getElementById('indicator').value = (resizerVal / 100).toFixed(2);
    });

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

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

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

    document.getElementById('copier').addEventListener('click', function() {
      navigator.clipboard.writeText('https://htmleditor.gitlab.io');
      function toggleNotification() {
        document.getElementById('notification').toggleAttribute('hidden');
      }
      toggleNotification();
      setTimeout(toggleNotification, 1500);
    });

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

    preview();
  </script>
</body>

</html>

And start from the top.

  1. The DOCTYPE being capitalized shows that you're still considering support for HTML4 or XHTML serializations. While it's case insensitive, the modern examples I've seen all leave it lowercase.
  2. While the charset is also case insensitive, it seems to be modern convention to leave it lowercase, as is in the Bootstrap examples.
  3. Separate your CSS into a file and link it like so: <link rel="stylesheet" href="styles.css">
  4. You favor ID selectors over class selectors, which IMO is a missed opprotunity here. Instead of repeatedly specifying elements, create a single CSS class and assign it to the elements you want the CSS to be applied to. The span element content for example is assigned to 7 other unique elements, which is not a good practice.
  5. The flex-direction, flex-wrap, align-items, and flex properties are all invalid as of CSS 3.0. Also 0px can just be 0. Here's the optimized CSS file:

styles.css

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

header,footer:not([hidden]) {
    display:flex;
    padding:5px
}

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

label,#downloader,select,#resetter,#selector,#fileChooser,output,span {
    color:#333;
    font:bold 11px Arial
}

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

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

select,button,#resizer {
    margin:0
}

#fileChooser {
    margin:0 auto 0 0
}

#resizer,iframe {
    padding:0
}

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

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

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

div {
    min-height:0;
    min-width:0
}

#viewerWrapper {
    border-left:5px solid #CCC
}

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

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

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

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

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

img {
    display:block
}

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

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

address a {
    color:inherit
}

body,main {
    display:flex
}
  1. Onto the HTML and JS, the most terrifying aspect here is the fact that user-inputted JS can run in the browser. In security consideration terms, this could open a door to XSS attacks. Fortunately this project is raw HTML with no external dependencies and run client-side, so any modern web browser should give protections by default. However sanitizing any given HTML would be an excellent practice. I'd also take a look at the other "best practices."
  2. It's unclear which JS version you're targeting, but assuming it's in the ballpark of ES2019 you should be using let over var so your variables are scoped to enclosed blocks.
  3. Your preview function is effectively acting like a main function. Create a proper main function that registers each listener so your element selector variables can be scoped properly. This again ties into security.
  4. I'd swap function() { iterations with () => { just because it's a bit cleaner.
  5. Your resetter event listener (the only significant logic here) can be simplified by checking if the editor value is default first, then placing the first block in an else-if with confirm as the condition.
  6. Separate your JS into a file and link it like so: <script type="text/javascript" src="editor.js"></script>
\$\endgroup\$
5
  • \$\begingroup\$ @Thanks for the review! #1: Done. #2: Done. #4: "The span definition is the primary offender." I'm not sure what you mean. #10: Would you mind showing me how to do it? \$\endgroup\$
    – Mori
    Commented May 14, 2023 at 10:23
  • \$\begingroup\$ Reference the edits. Also, don't get in the habit of mindlessly copy-pasting code, because you could be copying over bugs as well. \$\endgroup\$
    – T145
    Commented May 14, 2023 at 17:39
  • \$\begingroup\$ "You favor ID selectors over class selectors" Actually, the only reason I have used an id in this document is to distinguish one element from similar ones so I can easily get it in my JavaScript code. Take the Select button, for example. Regarding the CSS, I don't really see why I should add a class to 13 elements rather than use the 8 selectors on the current stylesheet. It reminds me of a question I asked years ago on a forum. I can't even come up with a semantic class name in this case. \$\endgroup\$
    – Mori
    Commented May 17, 2023 at 7:55
  • \$\begingroup\$ "Also 0px can just be 0" According to MDN, a preferred size of 0 must have a unit. Please try it in action, and you'll see the difference. "The flex-direction, flex-wrap, align-items, and flex properties are all invalid as of CSS 3.0." I can't find it on your linked reference. By the way, things are broken when I use your optimized CSS file. And your reset function is more simplified, but it has a problem: The confirm dialog shouldn't appear when the textarea value is an empty string. \$\endgroup\$
    – Mori
    Commented May 17, 2023 at 8:24
  • \$\begingroup\$ @Mori 1) I'm not saying to add classes to replace selectors. 2) That applies for flex-basis, which you don't use. It also requires a unit to be supplied b/c it uses absolute lengths. 3) How so? Which browser are you using? 4) Yes; that was intentional so you wouldn't get in the habit of copy-pasting. ;) \$\endgroup\$
    – T145
    Commented May 17, 2023 at 18:30
3
\$\begingroup\$

Your code is very cool! Definitely better than my editor... Anyway, here are some points to fix your code to be kind of better:

  1. Instead of a textarea, maybe use a stylized code editor like CodeFlask or maybe even CodeMirror rather than just a simple <textarea>.
  2. Instead of using a forEach on an array, I think it'd be better to just do a for (event of ["click", "contextmenu"]) like this:
for (event of ["click", "contextmenu"]) {
    downloader.addEventListener(event, function() {
        var blob = new Blob([editor.value], {type: "text/html"})
        this.href = URL.createObjectURL(blob)
    })
}
  1. In very low height, a scrollbar appears when the "footer" is shown.
  2. Speaking about the sharer, you can add an additional button that uses the navigator.share() functionality

And that's it! I can't think of any more original suggestions. (I'm surprised no one talked about these suggestions)

\$\endgroup\$
2
  • \$\begingroup\$ @Thanks for the review! #2: Done. #3: Done. #4: The navigator.share() method offers limited share options. \$\endgroup\$
    – Mori
    Commented Aug 8, 2023 at 4:31
  • \$\begingroup\$ OK! Thanks for considering my edits ;) \$\endgroup\$ Commented Sep 21, 2023 at 9:03
2
\$\begingroup\$

I had wanted to be able to keep notes on my computer in a basic text file, including math symbols, which is not simple, and sometimes not possible, even using the unicode character set. Then I discovered MathJax. HTML, with extensions like MathJax, seems to be the most general solution to generating free-form notes from an ASCII keyboard. To that goal, I began something like this real-time html editor over a decade ago, but never followed-through with loading and saving the edited files, so this solution is very much appreciated!

Supposing that MathJax is being used in an html page being edited, there arises the complication of writing dynamic content. MathJax normally runs its typesetting function only once, when the page first loads. Dynamic MathJax content requires additional code, and would be a very nice feature to have here.

It turns out that this can be addressed exclusively in the user's document, but can be handled much more elegantly by adding a "gating" function to HTML Editor which checks for the presence of MathJax and delays the document rendering until the user moves their mouse cursor into, or outof and into, the render iframe. There is further discussion at https://github.com/mathjax/MathJax/issues/3094, Scroll Position corruption.

Reading at MathJax in Dynamic Content, and then at Handling Asynchronous Typesetting, these considerations only apply to a document being modified in place - which is not what is happening here.

After looking more carefully at this real-time editor, the editor itself simply loads the edited document and then the browser renders this version as an entirely new document. The real-time editor is not making incremental changes to the Document Object Model, and there is no "extra" work needed from the real-time editor to manage MathJax.

Still, to avoid re-running the MathJax typesetting function after every character entry, it is easiest to simply uncheck the "Run" check box during editing, and then enable "Run" again to render the modified document, including running the MathJax typesetting function, as always.

An alternative - which requires no change to the real-time editor - involves modifying the user document, with the advantage of not needing to bother with the "Run" checkbox. The idea is to move the mouse cursor outside of the iframe, then run the MathJax typesetting function once and only after the mouse cursor moves back onto the rendered document iframe, using some MathJax code from Configuring and Loading in One Script. This simply requires modifying the user document MathJax head element, for instance, like this:

<head>
...

<!--
<script type="text/javascript" id="MathJax-script" async
  src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js">
</script>
 -->

<script>
  document.querySelector('html').onmouseenter = function() {
   var scriptx = document.createElement('script');
   scriptx.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js';
//   scriptx.src = 'mathjax/tex-chtml-full.js';
   scriptx.async = true;
   document.head.appendChild(scriptx);
  };
</script>

...
</head>

The biggest issue I notice with HTML Editor is that whenever the textarea source is edited, the display position in the rendered iframe jumps to the top of the page. This has the effect of the user never being able to see what has just been edited without having to repeatedly scroll back to the original edit position, which is tiresome in a long document. The solution is to save, and then restore, the iframe scroll position in the preview() function.

When running MathJax, restoring the scroll position is not perfect. The resulting scroll position will be close to the same position as before the editing, after MathJax has finished its typesetting, but the position will not be the same as before. It turns out that this is because simply saving and restoring the scroll position will position the user's document before MathJax has finished its typesetting.

The more elegant solution requires modifying HTML Editor as mentioned above, in addition to adding the proper MathJax header into the user's document. The modification to HTML Editor, including the MathJax "gating" function and changing the name of the original preview() function to ifpreview(), looks like this:

...
    var mjax = false;

    function preview() {
      if (!mjax) {
        ifpreview();
      } else {
        console.log("mjax = " + mjax);
        document.querySelector('iframe').addEventListener("mouseenter", ifpreview);
      }
    }

    function ifpreview() {
      if (run.checked) {

        let myIframe = document.querySelector('iframe');

        let xscroll = myIframe.contentWindow.scrollX;
        let yscroll = myIframe.contentWindow.scrollY;

        console.log("ifpreview mjax = " + mjax);
        const iframex = document.createElement('iframe'); // a fresh iframe to delete JavaScript variables
        document.querySelector('iframe').replaceWith(iframex);
        iframex.contentWindow.mjx = {xscroll, yscroll};

...

Altogether, the proper MathJax headers added in the user's document then look like this:

...
<script>
  MathJax = {

// run scrollTo() a second time after MathJax typesetting is complete
    startup: {
      pageReady: () => {
        return MathJax.startup.defaultPageReady().then(() => {
          scrollTo({
            left: mjx.xscroll,
            top: mjx.yscroll,
            behavior: "smooth"
          });
          console.log("Second Scroll: " + mjx.xscroll + " " + mjx.yscroll);
          parent.mjax = true;
        });
      },
  ...
  };
...
</script>
...
<script>
  document.querySelector('html').onmouseenter = function() {
   var scriptx = document.createElement('script');
   scriptx.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js';
//   scriptx.src = 'mathjax/tex-chtml-full.js';
   scriptx.async = true;
   document.head.appendChild(scriptx);
  };
</script>

BTW, I would prefer the traditional movable iframe divider, over the "Textarea size" slider, which can be a bit "twitchy". A simple alternative, still using your clever frame divider solution, is to spread the slider across the entire screen. With a vertical divider, the slider knob and the divider bar track very closely, and the control is then not so "twitchy". This change can be done by leaving off the label and output elements, moving the input element to last in the header, and using this style:

<input type="range" id="textareaSize" style="width:100%;height:5px;">

The same could be done in horizontal mode, first creating a new position for the slider, outside of the header, and adding orient="vertical" to the input style. I'm too lazy to work this out for now, and the full-width horizontal slider is good enough for both display modes.

Also, the footer links, except for "Feedback" and "Created by", are all getting jumbled on top of each other by Firefox Nightly on linux, which does not seem to handle the img alt attribute gracefully. One alternative is to add float: left; overflow: hidden; to the img style. A better looking alternative is to move the img alt attribute text into the anchor content proper, after the img tag. Hmm - the coding style in the page is often using the same exact text string as 1) an HTML Element or 2) an element Attribute or 3) a Document Object Model Property for 4) an element ID, and 5) a javascript Variable. I find that this style can be misleading and prefer visually distinguishing the strings for an ID or Variable from an Element or Attribute or Property, and from each other.

In the "Reset" button function, the line download.download = 'template.html'; appears to be redundant, since this value has already been set in the corresponding Anchor Attribute. This value is not being changed globally by the "input file"/"Browse" button function, in download.download = file.name;.

I find the Anchor tag content "Download" used in the page header to be misleading. The connotation of the term "Download" is that this link will transfer some data from a remote location to the browser. But in fact, the exact opposite function is being performed, where the local browser content is instead being sent to a "remote" location. In this case, the "local" content is being "downloaded" and the user, usually, is then prompted to save their local content to file.

A more intuitive moniker for this Anchor tag would be "Save", rather than "Download", preferably styled as a button, in similarity to the other function buttons in the page header.

This can be done by simply replacing the Anchor content, as:

<a href="" download="template.html" title="Save your HTML document" id="download"><button>Save</button></a>

Following is a version of the Real-Time HTML Editor with my changes, including the code to restore scroll position while editing. Running the code snippet here will not actually render the document in the iframe, so, to use it, the code needs to be recombined, with CSS in the <head>, between <style>...</style> tags, and the javascript inside the <body>, between <script>...</script> tags. You can play with the slider, though.

const run = document.getElementById('run'),
  textarea = document.querySelector('textarea'),
  download = document.getElementById('download'),
  chooseFile = document.getElementById('chooseFile');

var mjax = false;

function preview() {
  if (!mjax) {
    ifpreview();
  } else {
    console.log("mjax = " + mjax);
    document.querySelector('iframe').addEventListener("mouseenter", ifpreview);
  }
}

function ifpreview() {
  if (run.checked) {

    let myIframe = document.querySelector('iframe');

    let xscroll = myIframe.contentWindow.scrollX;
    let yscroll = myIframe.contentWindow.scrollY;

    console.log("ifpreview mjax = " + mjax);
    const iframex = document.createElement('iframe'); // a fresh iframe to delete JavaScript variables
    document.querySelector('iframe').replaceWith(iframex);
    iframex.contentWindow.mjx = {
      xscroll,
      yscroll
    };

    const iframeDoc = iframex.contentDocument;
    iframeDoc.write(textarea.value);
    iframeDoc.close();

    document.querySelector('iframe').contentWindow.scrollTo({
      left: xscroll,
      top: yscroll,
      behavior: "instant"
    });

  }
}

textarea.addEventListener('input', preview);

run.addEventListener('change', preview);

for (const eventx of ['click', 'contextmenu']) {
  download.addEventListener(eventx, 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 = '';
    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;
  document.getElementById('textareaWrap').style.flexGrow = range;
  document.getElementById('iframeWrap').style.flexGrow = 100 - range;
});

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

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

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

document.getElementById('copy').addEventListener('click', function() {
  navigator.clipboard.writeText(location);

  function toggleNotification() {
    document.getElementById('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%;
  background: #777;
}

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;
  background: #777;
}

main.horizontal {
  flex-direction: column;
}

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

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

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

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

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;
  float: left;
  overflow: hidden;
}

#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;
}
<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Edit your HTML, CSS, and JavaScript code and monitor the instant live preview.">
  <title>HTML Editor: online HTML editor with real-time preview</title>
  <link rel="icon" href="favicon.ico">
</head>

<body>

  <header>
    <label for="run">Run</label>
    <input type="checkbox" id="run" checked>
    <a href="" download="template.html" title="Save your HTML document" id="download"><button>Save</button></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="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>

    <input type="range" id="textareaSize" style="width:100%;height:5px;">

  </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>

  <p>While editing documents containing MathJax markup, uncheck the "Run" checkbox, or run MathJax
  typesetting conditionally, by moving the mouse cursor into, or outof and into, the rendered page window, and
  including the following Javascipt in the head of your document:</p>

  <div style="background: #f7fff7">
  <pre>
&amp;lt;head>
...
&amp;lt;script>
  MathJax = {

// run scrollTo() a second time after MathJax typesetting is complete
    startup: {
      pageReady: () => {
        return MathJax.startup.defaultPageReady().then(() => {
          console.log("Second Scroll: " + mjx.xscroll + " " + mjx.yscroll);
          scrollTo({
            left: mjx.xscroll,
            top: mjx.yscroll,
            behavior: "smooth"
          });
        });
      }
    },
  ...
  };
...
&amp;lt;/script>
...
&amp;lt;script>
  document.querySelector('html').onmouseenter = function() {
   var scriptx = document.createElement('script');
   scriptx.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js';
//   scriptx.src = 'mathjax/tex-chtml-full.js';
   scriptx.async = true;
   document.head.appendChild(scriptx);
  };
&amp;lt;/script>
...
&amp;lt;/head>
  </pre>
  </div>

  <p>Test your browser's scroll position stability while editing by watching the position of these long
  lines:</p>

aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>
ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>

<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/twitter.svg" width="16" height="16">Twitter</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">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">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">Email</a>

    <button type="button" id="copy">
     <img src="images/link.svg" width="16" height="16">Link</button>
    <span id="notification" hidden>Copied!</span>

    <address><a href="https://codereview.stackexchange.com/questions/284918/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>

</body>

</html>

To address your question, and paraphrasing:

Including certain Math Symbols in a simple text file is not possible using the unicode character set. Can you provide an example?

A quick example of graphemes which are commonly seen in mathematical expressions and which are not to be found in the current Unicode character set can be found in "Proposal to encode missing Latin small capital and modifier letters in the UCS", dated 2011 March 12, https://www.unicode.org/L2/L2011/11208-n4068.pdf . The "missing" graphemes there have the notation "This character is proposed here." However, this proposal was never accepted by the Unicode Consortium.

In particular, consider "U+A7F3 MODIFIER LETTER SMALL Q" and "U+ABB5 LATIN SUBSCRIPT SMALL LETTER Q". These are the superscript and subscript versions of the lower case letter "q". This letter is commonly used, for instance, to express the signature of a Clifford Algebra, as a subscript "p,q,r", or might be used as abstract indexes of a Tensor. There are unicode code points for the subscript lower case letters "p" and "r", but nothing for the subscript letter "q". As a consequence, that abstract signature or Tensor index cannot be represented directly from the unicode character set.

Similarly, there is a unicode code point for the "dagger" grapheme, "U+2020 Dagger", but there is no such unicode code point for the "superscript dagger" grapheme, which might otherwise be used to indicate the "Reverse" of a Multivector Geometric Product. However, the Unicode Consortium does concede at times. For instance, there now exists a code point for the superscript lower case letter "q", "U+107A5 MODIFIER LETTER SMALL Q", and some others. But, subscript "q" is still not possible.

Just as with the principle distinction between "CSS vs HTML"/"style vs basis"/"skin vs skeleton", Unicode strives to encode only the basis of all written language, with only a single code point for every essential grapheme. In conflict, the historic foundation of Unicode also required that it subsume all then existing character encodings, to guarantee the complete fidelity of "forward and reverse"/"round-trip" conversions between Unicode and other competing encodings, leaving remnants of confusion, for which see https://en.wikipedia.org/wiki/Duplicate_characters_in_Unicode .

Many, but not all, people consider mathematical superscripts and subscripts to be strictly "style" and not ever "basis", especially because mathematical superscripts and subscripts may cascade, as, for instance, in
e - t/t0 .

Arguments about the use of unicode characters in mathematical expressions continue to the present day. For instance, https://corp.unicode.org/pipermail/unicode/2023-March/010472.html . There also exists a Unicode Consortium FAQ addressing the issue, https://unicode.org/faq/ligature_digraph.html#Pf8 , and reference https://unicode.org/reports/tr25 , which see. There are also numerous stackexchange/stackoverflow questions addressing the topic, for instance https://stackoverflow.com/questions/73409631/why-is-there-no-super-or-subscript-q-or-q-characters-defined-in-utf-8 .

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Thanks for the review! "I would prefer the traditional movable iframe divider" That's not keyboard and mobile accessible. Here is an example. "This value is not being changed globally by the 'input file/Browse' button function" It does change: Try uploading a file named test.html, for example, and then click the Download link. The suggested file name by the browser will be test.html, so the reset function needs to reset the anchor's download attribute value as well. \$\endgroup\$
    – Mori
    Commented Aug 25, 2023 at 17:42
  • \$\begingroup\$ "The connotation of the term 'Download' is that this link will transfer some data from a remote location to the browser." For that purpose, I'd put a URL button next to the file input. "A more intuitive moniker for this Anchor tag would be 'Save', rather than 'Download'" The word "save" can also be used when the latest changes are saved on the editor/server for later access, which is not the case here, so it can cause confusion. Here is an example. \$\endgroup\$
    – Mori
    Commented Aug 25, 2023 at 17:44
  • \$\begingroup\$ "preferably styled as a button, in similarity to the other function buttons in the page header." Yes, it would be more consistent, but it's a link, not a button: A button would only ask for a click. The user should know they can also right-click and open the link in a new tab to see the pure page they have created. "sometimes not possible, even using the unicode character set" Can you provide an example? \$\endgroup\$
    – Mori
    Commented Aug 25, 2023 at 17:45
  • 1
    \$\begingroup\$ By the way, I wouldn't worry about Firefox Nightly on Linux and its bugs as only a really small portion of people use it. \$\endgroup\$
    – Mori
    Commented Aug 25, 2023 at 23:33
1
\$\begingroup\$

Main updates

  1. Simplified the reset function.
  2. Removed extra IDs.
  3. Updated the preview function.
  4. Replaced var variables with const.
  5. Removed const iframe = document.createElement('iframe'); from the function preview(). It's not needed.
  6. Defined the elements used in the functions in the global scope to improve the performance.

Latest 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/twitter.svg" width="16" height="16" alt="Twitter"></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/284918/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\$

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.