22
\$\begingroup\$

HTML Editor is an online HTML editor with a minimalist approach. Edit your HTML, CSS, and JavaScript code and monitor the instant live preview.

Please review the source code and provide feedback:

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

<head>
    <meta charset="UTF-8">
    <meta name="description" content="An online HTML editor with real-time preview">
    <title>HTML Editor</title>
    <link rel="icon" href="favicon.ico" type="image/x-icon">
    <base target="_blank">
    <style>
        html,
        body {
            margin: 0;
            padding: 0;
            height: 100%;
        }
        body {
            display: -webkit-flex;
            /* WebKit prefixes are added to support Safari. */
            display: flex;
            -webkit-flex-direction: column;
            flex-direction: column;
        }
        header,
        .shown {
            display: -webkit-flex;
            display: flex;
            -webkit-align-items: center;
            align-items: center;
            padding: 5px;
        }
        header {
            background: linear-gradient(#FFF, #CCC);
        }
        #fileSaver,
        [type="button"],
        #fileChooser,
        label,
        span {
            font: bold 11px arial;
            color: #333;
        }
        #selector,
        #resizer,
        #viewsToggle,
        [title$="Twitter"] {
            margin-right: 5px;
            margin-left: 5px;
        }
        #fileSaver {
            margin-right: 5px;
        }
        #fileChooser,
        [title$="Facebook"] {
            margin-right: auto;
        }
        #resizer {
            margin-top: 0;
            margin-bottom: 0;
            padding: 0;
        }
        /* to remove the extra margins and padding in some browsers, e.g. IE11 */
        span {
            width: 35px;
        }
        #footerToggle {
            margin-right: 0;
            margin-left: 5px;
            border: 0;
            padding: 0;
            background: transparent;
        }
        main {
            display: -webkit-flex;
            display: flex;
            -webkit-flex: 1;
            flex: 1;
        }
        .horizontal {
            -webkit-flex-direction: column;
            flex-direction: column;
        }
        main * {
            margin: 0;
            -webkit-flex: 50;
            flex: 50;
            background: #FFF;
            min-height: 100%;
            /* to ensure that the flex items are stretched to use available space; IE11, for example, doesn't stretch the iframe. */
        }
        .horizontal * {
            min-width: 100%;
            min-height: 0;
            /* to get back to the initial value */
        }
        textarea {
            box-sizing: border-box;
            border: 0;
            outline: 0;
            padding: 5px;
            resize: none;
            overflow: auto;
            /* to remove the default scrollbar in IE11 */
        }
        .minSize {
            padding: 0;
        }
        iframe {
            border: solid #CCC;
            border-width: 0 0 0 5px;
            padding: 0;
        }
        .horizontal iframe {
            border-width: 5px 0 0;
        }
        .shown {
            background: linear-gradient(#CCC, #FFF);
        }
        img {
            display: block;
            width: 20px;
            height: 20px;
        }
        address,
        address a {
            color: #333;
        }
    </style>
</head>

<body>
    <header>
        <a download="myFile.html" title="Save as..." id="fileSaver">Save as...</a>
        <input type="button" value="Reset" id="resetter">
        <input type="button" value="Select" id="selector">
        <input type="file" accept="text/html" id="fileChooser">
        <label for="resizer">Text field size</label>
        <input type="range" id="resizer">
        <span id="indicator">50%</span> 
        <!-- The semantic element to use instead of span is output. But it's not supported in IE11. -->
        <label for="viewsToggle">Horizontal view</label>
        <input type="checkbox" id="viewsToggle">
        <input type="button" value="▲" title="Show footer" id="footerToggle">
    </header>
    <main id="main">
        <textarea spellcheck="false" id="editor"><!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>HTML Document Template</title>
  </head>
  <body>
    <p>Hello, world!</p>
  </body>
</html></textarea>
        <iframe id="viewer"></iframe>
    </main>
    <footer hidden id="footer">
        <a href="https://plus.google.com/share?url=http%3A%2F%2Fhtmleditor.gitlab.io%2F" title="Share on Google+">
            <img src="images/google+.png" alt="Google+">
        </a>
        <a href="https://twitter.com/share?text=HTML%20Editor&amp;url=http%3A%2F%2Fhtmleditor.gitlab.io%2F" title="Share on Twitter">
            <img src="images/twitter.png" alt="Twitter">
        </a>
        <a href="https://www.facebook.com/sharer.php?u=http%3A%2F%2Fhtmleditor.gitlab.io%2F" title="Share on Facebook">
            <img src="images/facebook.png" alt="Facebook">
        </a>
        <address><a href="feedback.html" title="Feedback">Feedback</a> / Created by <a href="https://plus.google.com/+MortezaMirmojarabian?rel=author" title="Google+ profile" rel="author">Mori</a></address>
    </footer>
    <script>
        var editor = document.getElementById('editor'),
            viewer = document.getElementById('viewer'),
            fileChooser = document.getElementById('fileChooser'),
            resizer = document.getElementById('resizer');

        function preview() {
            try {
                var viewerDoc = viewer.contentDocument;
                viewerDoc.open();
                viewerDoc.write(editor.value);
                viewerDoc.close();
            } catch (e) { // in case of iframe redirection to a different origin
                viewer.src = 'about:blank';
                setTimeout(preview, 4); // minimum delay
            }
        }
        preview();
        editor.oninput = preview;

        function createURL() {
            var blob = new Blob([editor.value], {
                type: 'text/html'
            });
            document.getElementById('fileSaver').href = window.URL.createObjectURL(blob);
        }
        createURL();
        editor.onchange = createURL;
        fileChooser.onclick = function () { // to empty the fileList so you can rechoose the same file
            this.value = '';
        };
        fileChooser.onchange = function () {
            var file = this.files[0],
                reader = new FileReader();
            if (file) { // to ensure that there's a file to read so IE11 doesn't run this function on clicking fileChooser before you choose a file
                reader.readAsText(file);
                reader.onload = function () {
                    editor.value = this.result;
                    preview();
                    createURL();
                };
            }
        };
        document.getElementById('viewsToggle').onchange = function () {
            document.getElementById('main').classList.toggle('horizontal');
        };
        resizer.oninput = resizer.onchange = function () { // The onchange property is added to support IE11.
            var resizerVal = this.value;
            editor.style.webkitFlex = resizerVal;
            editor.style.flex = resizerVal;
            viewer.style.webkitFlex = 100 - resizerVal;
            viewer.style.flex = 100 - resizerVal;
            document.getElementById('indicator').textContent = resizerVal + '%';
            if (resizerVal == 0) {
                editor.className = 'minSize';
            } else {
                editor.className = '';
            }
        };
        document.getElementById('selector').onclick = function () {
            editor.select();
        };
        document.getElementById('resetter').onclick = function () {
            if (!editor.value || editor.value != editor.defaultValue && confirm('Are you sure?')) {
                editor.value = editor.defaultValue;
                preview();
                createURL();
            }
        };
        document.getElementById('footerToggle').onclick = function () {
            var footerClasses = document.getElementById('footer').classList;
            footerClasses.toggle('shown');
            if (footerClasses.length) {
                this.value = '▼';
                this.title = 'Hide footer';
            } else {
                this.value = '▲';
                this.title = 'Show footer';
            }
        };
    </script>
</body>

</html>
\$\endgroup\$
17
\$\begingroup\$
  • Consider escaping the content of the textarea. Seeing the differently-indented </html> made me stumble in my scan-through, whereas &lt;/html&gt would not have. It's also good practice... Incidentally, is there a reason why your code is indented with 4 spaces, but your default textarea value uses 2?

  • Some people like to indent with tabs. You need to capture the keypress.

  • Internet Explorer 9 and under does not have support for <input type="range" />. Your panes will not be resizeable in that case, and the Text field size control will be confusing (though functional).

    • Upon further review, this isn't very relevant because your site is completely broken in IE9. Blob is unavailable, and halts execution of the script. At the very least, you should display a message indicating that the browser is unsupported.

Aside from that, the code seems pretty good. However, from a user standpoint:

  • The Save as... control is a link, but all the others are buttons. This seems a bit inconsistent.

  • If I choose a file and then reset, I expect that it resets to the file I had selected. If this is not the case, then the form control should reset to no value, so I see "No file chosen" rather than my file name.

  • The footer shower/hider triangle doesn't make much sense to me. There's little reason to hide the footer, and even less so to show it if it's hidden by default. Even if there was, I'd prefer for the hide button to be on the footer itself.

  • It would be nice if the panes could be resized with the separator instead of the range input.


After your edit, I took another look and noticed a few more things:

  • Although your file input has accept="text/html", the user can still choose to upload any file. Consider validating file.type in fileChooser.onchange().

  • It would be nice if before choosing a file, if I have unsaved changes, I get a confirmation message.

  • In the same vein, if I have unsaved changes, maybe set an onbeforeunload?

The following would implement the last two points. Note that save detection is naive, since as far as I'm aware, there's no way to conclusively determine if a user has saved the file to disk. (You'd also have to set changed = false in reader.onload() and resetter.onclick().)

var changed = false;
editor.oninput = function() {
    changed = true;
    preview();
}
fileChooser.onclick = function() {
    return changed && confirm("Your changes will be lost if you select another file. Are you sure you want to continue?");
}
window.onbeforeunload = function() {
    return changed ? "You have unsaved changes. Are you sure you want to leave this page?" : undefined;
}
fileSaver.onclick = function() {
    changed = false;
}
\$\endgroup\$
  • 3
    \$\begingroup\$ At the very least, you should display a message indicating that the browser is unsupported: it would be even better if @Mori detected the capability for the features, instead of detecting browser+version. \$\endgroup\$ – ANeves Jul 8 '14 at 18:27
  • 2
    \$\begingroup\$ @Mori the percentage of IE9 users is bigger than you think. Caniuse claims IE9 is at 2.6% while Safari (which you specifically mention in comments) is only at 1.64%. Sad truth is, IE8 is even more popular at 4.46%. That's an unacceptably large number of users to give the finger to. Plus, if it is supposed to be a tool for allowing users to design prototypes, the fact that some browsers cannot be tested with it is a big deal. \$\endgroup\$ – cimmanon Jul 14 '14 at 11:09
  • 1
    \$\begingroup\$ @Schism: Thanks for the suggestion, but I want nothing extra/distracting in the editor, let alone in the viewer, where you see the result of your coding: the preview should be pure. \$\endgroup\$ – Mori Jan 10 '16 at 3:18
  • 1
    \$\begingroup\$ "If this is not the case, then the form control should reset to no value, so I see "No file chosen" rather than my file name." Makes sense! Corrected! "The footer shower/hider triangle doesn't make much sense to me." I couldn't find a better way to hide & show the footer. I decided to hide the footer as I find it rather distracting and not directly relevant to the main task of HTML Editor. Besides, it takes up some space that you can use for coding. Things on the footer, e.g. my Google+ profile, aren't things you need all the time. They're probably used only once. \$\endgroup\$ – Mori May 22 '17 at 16:52
  • 1
    \$\begingroup\$ @Schism: According to the spec, the accept attribute may be specified to provide user agents with a hint of what file types will be accepted. The accept attribute filters the files you see in the Open dialog so you can easily find and work with the desired files among the clutter while you can still select All Files and see/select other files. Then this attribute isn't meant to be a validation tool. Although HTML Editor is an HTML ediotr, you can use it to open, edit and save any text file. \$\endgroup\$ – Mori May 22 '17 at 17:04
11
\$\begingroup\$

In all, quite impressive code, here are some pointers:

  • Besides the suggestion to compare with 0 via ===, JsHint could not find anything
  • Consider using addEventListener instead of the old skool onxxx
  • I would put preview(); and createURL(); together instead of having them in the middle of event handler assignments, either right after the var assignments or at the very bottom.
  • Or you could even consider faking a click on 'resetter' which does all that, if you dont use addEventListener, you could simply do

    document.getElementById('resetter').onclick();  
    
  • Ternary can be your friend here:

     if (resizerVal == 0) {
         editor.className = 'minSize';
     } else {
         editor.className = '';
     }
    

    can be

     editor.className = resizerVal ? '' : 'minSize';
    
\$\endgroup\$
  • \$\begingroup\$ "I would put preview(); and createURL(); together" I've called these functions immediately after defining them for a better code human-readability. "Or you could even consider faking a click on 'resetter' which does all that" I'm not sure what you mean. Would you mind providing a sample? "Ternary can be your friend here" I'm not familiar with Ternary and should search for it. Thanks for the suggestion! \$\endgroup\$ – Mori Jul 9 '14 at 9:01
  • \$\begingroup\$ Ternary is a link now, full name is ternary operator. Added a ( hackish ) example of what I mean for faking a click. The Google should help you in case you want to use addEventListener. \$\endgroup\$ – konijn Jul 9 '14 at 13:01
  • \$\begingroup\$ Just learned about ternary operator. It should be editor.className = resizerVal == 0 ? 'minSize' : '';. \$\endgroup\$ – Mori Jul 10 '14 at 9:32
  • \$\begingroup\$ I my example I evaluate resizerVal itself, if it is not 0 it will be truthy, if it is 0 it will be falsy, try it. \$\endgroup\$ – konijn Jul 10 '14 at 12:58
  • \$\begingroup\$ I tried it to no avail. I think that's because an <input> value is a string and can never be falsy, even if you set it to zero. \$\endgroup\$ – Mori Jul 11 '14 at 0:19
3
\$\begingroup\$

Main updates:

  1. Indented code with two spaces rather than four
  2. Changed the download attribute value, myFile.html, to something meaningful: template.html
  3. Combined the preview and createURL functions in a new function
  4. Removed the fileChooser.onclick function: this function is done by the Reset button now.
  5. Added downloader.download = file.name; to the fileChooser.onchange function so the downloader download attribute has the same value as the imported file name. Now there's a logical relationship between the text field content, fileChooser value, and the downloader download attribute value.
  6. In the resize function, changed flex to flexGrow as flexShrink and flexBasis never change
  7. Defined a new task for the Reset button so it not only resets the text field, but also the fileChooser and downloader download attribute values
  8. Added a confirmation message on page exit if the text field is modified
  9. Added a new option: Dark theme; made some code improvements
  10. Added flex-wrap: wrap; to the header and footer so the flex items wrap if necessary
  11. Added a run-stop toggle switch:
    • Sometimes you shouldn't run your code until you finish coding or else it will crash your browser. For example, when you're writing loops it can cause an infinite loop.
    • You might want to re-run your code and re-see the result when working with CSS animations, for example. You can achieve it by double-clicking the Run checkbox.
  12. Now Edge supports outputObject.value — changed indicator.textContent to indicator.value.

Credit:

Special thanks to Schism for his detailed pointers!

Final source code:

<!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">
  <base target="_blank">
  <style>
    html,
    body {
      margin: 0;
      padding: 0;
      height: 100%;
    }

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

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

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

    label,
    #downloader,
    [type="button"],
    #fileChooser,
    output {
      font: bold 11px Arial;
      color: #333;
    }

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

    #resetter,
    #resizer {
      margin: 0 5px;
    }

    #selector {
      margin: 0;
    }

    #fileChooser {
      margin: 0 auto 0 5px;
    }

    #resizer,
    iframe {
      padding: 0;
    }

    output {
      width: 3.5ch;
      margin-right: 10px;
    }

    #more {
      margin: 0;
      border: 0;
      padding: 0;
      background: transparent;
    }

    #more.on {
      border-bottom: 2px solid;
      margin-bottom: -2px;
    }

    main {
      flex: 1;
      display: flex;
    }

    main.horizontal {
      flex-direction: column;
    }

    div {
      flex-basis: 0;
      position: relative;
    }

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

    main.horizontal #viewerWrapper {
      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;
      color: #333;
    }

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

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

    [title$="Twitter"] {
      margin-left: 5px;
      margin-right: 5px;
    }

    img {
      display: block;
      width: 20px;
      height: 20px;
    }

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

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

<body>
  <header>
    <label for="runner">Run</label>
    <input type="checkbox" checked id="runner">
    <a download="template.html" title="Download the HTML document" id="downloader">Download</a>
    <input type="button" value="Reset" id="resetter">
    <input type="button" value="Select" id="selector">
    <input type="file" accept="text/html" id="fileChooser">
    <label for="resizer">Editor size</label>
    <input type="range" id="resizer">
    <output for="resizer" id="indicator"></output>
    <label for="viewsToggler">Horizontal view</label>
    <input type="checkbox" id="viewsToggler">
    <label for="themesToggler">Dark theme</label>
    <input type="checkbox" id="themesToggler">
    <input type="button" value="•••" title="More" id="more">
  </header>
  <main id="main">
    <div id="editorWrapper">
      <textarea spellcheck="false" id="editor"><!DOCTYPE html>
<html lang="en">
  <head>
    <title>HTML Document Template</title>
  </head>
  <body>
    <p>Hello, world!</p>
  </body>
</html></textarea>
    </div>
    <div id="viewerWrapper">
      <iframe id="viewer"></iframe>
    </div>
  </main>
  <footer hidden id="footer">
    <a href="https://plus.google.com/share?url=http%3A%2F%2Fhtmleditor.gitlab.io%2F" title="Share on Google+">
      <img src="images/google+.png" alt="Google+">
    </a>
    <a href="https://twitter.com/share?text=HTML%20Editor%3A%20online%20HTML%20editor%20with%20real-time%20preview&amp;url=http%3A%2F%2Fhtmleditor.gitlab.io%2F" title="Share on Twitter">
      <img src="images/twitter.png" alt="Twitter">
    </a>
    <a href="https://www.facebook.com/dialog/share?app_id=664554287087112&amp;href=http%3A%2F%2Fhtmleditor.gitlab.io%2F" title="Share on Facebook">
      <img src="images/facebook.png" alt="Facebook">
    </a>
    <address><a href="https://codereview.stackexchange.com/questions/56106/html-editor-online-html-editor-with-real-time-preview" title="javascript - HTML Editor: online HTML editor with real-time preview - Code Review Stack Exchange">Feedback</a> | Created by <a href="http://google.com/+MortezaMirmojarabian" title="Morteza Mirmojarabian - Google+" rel="author">Mori</a></address>
  </footer>
  <script>
    var runner = document.getElementById('runner'),
      editor = document.getElementById('editor'),
      downloader = document.getElementById('downloader'),
      fileChooser = document.getElementById('fileChooser'),
      resizer = document.getElementById('resizer'),
      viewsToggler = document.getElementById('viewsToggler'),
      themesToggler = document.getElementById('themesToggler');

    function preview() {
      if (runner.checked) {
        var viewer = document.getElementById('viewer');
        try {
          var viewerDoc = viewer.contentDocument;
          viewerDoc.open();
          viewerDoc.write(editor.value);
          viewerDoc.close();
        } catch (e) { // in case of iframe redirection to a different origin
          viewer.src = 'about:blank';
          setTimeout(preview, 4); // minimum delay
        }
      }
    }
    editor.addEventListener('input', preview);
    runner.addEventListener('change', preview);

    function createURL() {
      var blob = new Blob([editor.value], {
        type: 'text/html'
      });
      downloader.href = window.URL.createObjectURL(blob);
    }
    editor.addEventListener('change', createURL);

    function previewAndCreateURL() {
      preview();
      createURL();
    }

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

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

    fileChooser.addEventListener('change', function() {
      var file = this.files[0],
        reader = new FileReader();
      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;
        reader.readAsText(file);
        reader.addEventListener('load', function() {
          editor.value = this.result;
          previewAndCreateURL();
        });
      }
    });

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

    function toggleViews() {
      var main = document.getElementById('main');
      if (viewsToggler.checked) {
        main.className = 'horizontal';
      } else {
        main.className = '';
      }
    }
    viewsToggler.addEventListener('change', toggleViews);

    function toggleThemes() {
      if (themesToggler.checked) {
        editor.className = 'dark';
      } else {
        editor.className = '';
      }
    }
    themesToggler.addEventListener('change', toggleThemes);

    document.getElementById('more').addEventListener('click', function() {
      if (this.title == 'More') {
        this.title = 'Less';
      } else {
        this.title = 'More';
      }
      this.classList.toggle('on');
      document.getElementById('footer').classList.toggle('shown');
    });

    window.addEventListener('beforeunload', function(event) {
      if (editor.value && editor.value != editor.defaultValue) {
        event.returnValue = 'Your changes may be lost.';
      }
    });

    resize();
    toggleViews();
    toggleThemes();
    previewAndCreateURL();
  </script>
</body>

</html>
\$\endgroup\$
  • \$\begingroup\$ I apologize for the hassle again. The FAQ also states that you should mention what was updated, so that it's still a valid code review. It should then be marked as Community Wiki. \$\endgroup\$ – Jamal Jul 29 '14 at 5:04
  • \$\begingroup\$ "you should mention what was updated" Mmm... Actually, I'm a little puzzled: haven't I mentioned it in my post? I've clearly stated that the reset function, for example, is changed. If you mean something else, would you mind doing it on behalf of me? Thanks, again! \$\endgroup\$ – Mori Jul 29 '14 at 5:08
  • \$\begingroup\$ I mean the individual changes made for each update (or just mentioning one or more answers if all advice was taken from them). \$\endgroup\$ – Jamal Jul 29 '14 at 5:09
  • \$\begingroup\$ Got it! I will. \$\endgroup\$ – Mori Jul 29 '14 at 5:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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