Overview
HTML Editor is a web-based HTML editor designed for web developers, designers, and learners. It offers a lightweight, minimalist environment for writing and previewing HTML, CSS, and JavaScript code in real-time. The editor also supports creating, opening, and editing various text-based file types, such as .txt
, .css
, .js
, .svg
, and more.
This project is hosted on GitLab and licensed under the MIT License.
Specific review requests
- Best practices: How well does my code adhere to best practices and coding conventions?
- Code structure and readability: Are there any improvements I can make to the organization and clarity of my code?
- Performance: Are there any optimizations that can enhance the performance of the editor?
- Features and usability: Do you have any suggestions for additional features or improvements to the existing ones?
Source code
const previewer = document.querySelector('iframe'),
editor = document.querySelector('textarea'),
previewSelector = document.getElementById('previewSelector'),
downloadLink = document.getElementById('downloadLink'),
fontSizeSelector = document.getElementById('fontSizeSelector'),
runButton = document.getElementById('runButton'),
fileInput = document.getElementById('fileInput'),
editorSizeInput = document.getElementById('editorSizeInput'),
editorWrapper = document.getElementById('editorWrapper'),
previewerWrapper = document.getElementById('previewerWrapper'),
editorSizeOutput = document.querySelector('output'),
verticalViewToggle = document.getElementById('verticalViewToggle'),
mainContainer = document.querySelector('main'),
darkEditorToggle = document.getElementById('darkEditorToggle'),
spellcheckToggle = document.getElementById('spellcheckToggle'),
footerContainerToggle = document.getElementById('footerContainerToggle'),
footerContainer = document.querySelector('footer'),
copyNotification = document.getElementById('copyNotification');
let previewTimeout = null;
function toggleClass(element, className, force) {
element.classList.toggle(className, force);
}
function preview() {
previewer.replaceWith(previewer); // a fresh iframe to delete JavaScript variables
const previewerDocument = previewer.contentDocument;
previewerDocument.write(editor.value);
previewerDocument.close();
}
function timedPreview() {
const previewSelectorValue = previewSelector.value;
if (previewSelectorValue === 'Instant') {
preview();
} else if (previewSelectorValue !== 'Manual') {
clearTimeout(previewTimeout);
previewTimeout = setTimeout(preview, previewSelectorValue);
}
}
function createDownloadURL() {
const blob = new Blob([editor.value], {
type: 'text/html; charset=utf-8'
});
downloadLink.href = URL.createObjectURL(blob);
}
function resizeFont() {
editor.style.fontSize = fontSizeSelector.value + 'px';
}
function toggleRunButton() {
toggleClass(runButton, 'hidden', previewSelector.value !== 'Manual');
}
function resizeEditor() {
const editorSizeInputValue = editorSizeInput.value;
editorWrapper.style.flexGrow = editorSizeInputValue;
previewerWrapper.style.flexGrow = 100 - editorSizeInputValue;
editorSizeOutput.value = (editorSizeInputValue / 100).toFixed(2);
}
runButton.addEventListener('click', preview);
editor.addEventListener('input', timedPreview);
downloadLink.addEventListener('click', createDownloadURL);
downloadLink.addEventListener('contextmenu', createDownloadURL);
fontSizeSelector.addEventListener('change', resizeFont);
previewSelector.addEventListener('change', toggleRunButton);
editorSizeInput.addEventListener('input', resizeEditor);
document.getElementById('resetButton').addEventListener('click', function() {
if (!editor.value || editor.value !== editor.defaultValue && confirm('Your input will be lost.\nAre you sure you want to reset?')) {
fileInput.value = '';
downloadLink.download = 'template.html';
editor.value = editor.defaultValue;
timedPreview();
}
});
document.getElementById('selectButton').addEventListener('click', function() {
editor.select();
});
fileInput.addEventListener('change', async function() {
const file = this.files[0];
if (file) { // to ensure that there's a file to read so Chrome, for example, doesn't run this function when you cancel choosing a new file
downloadLink.download = file.name;
editor.value = await file.text();
timedPreview();
}
});
verticalViewToggle.addEventListener('click', function() {
toggleClass(mainContainer, 'vertical');
toggleClass(this, 'active');
});
darkEditorToggle.addEventListener('click', function() {
toggleClass(editor, 'dark');
toggleClass(this, 'active');
});
spellcheckToggle.addEventListener('click', function() {
editor.spellcheck = !editor.spellcheck;
toggleClass(this, 'active');
});
footerContainerToggle.addEventListener('click', function() {
toggleClass(footerContainer, 'hidden');
toggleClass(this, 'active');
});
document.getElementById('copyButton').addEventListener('click', function() {
navigator.clipboard.writeText(location);
toggleClass(copyNotification, 'hidden');
setTimeout(toggleClass, 2000, copyNotification, 'hidden');
});
// Save the current state to localStorage.
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
const currentState = {
spellcheckToggle: spellcheckToggle.className,
verticalViewToggle: verticalViewToggle.className,
darkEditorToggle: darkEditorToggle.className,
footerContainerToggle: footerContainerToggle.className,
previewSelector: previewSelector.value,
fontSizeSelector: fontSizeSelector.value,
editorSizeInput: editorSizeInput.value,
editor: editor.value,
};
localStorage.setItem('state', JSON.stringify(currentState));
}
});
// Restore the state from localStorage.
const restoredState = JSON.parse(localStorage.getItem('state'));
if (restoredState) {
spellcheckToggle.className = restoredState.spellcheckToggle;
verticalViewToggle.className = restoredState.verticalViewToggle;
darkEditorToggle.className = restoredState.darkEditorToggle;
footerContainerToggle.className = restoredState.footerContainerToggle;
previewSelector.value = restoredState.previewSelector;
fontSizeSelector.value = restoredState.fontSizeSelector;
editorSizeInput.value = restoredState.editorSizeInput;
editor.value = restoredState.editor;
}
// Update the UI based on the restored state.
editor.spellcheck = spellcheckToggle.classList.contains('active');
toggleClass(mainContainer, 'vertical', verticalViewToggle.classList.contains('active'));
toggleClass(editor, 'dark', darkEditorToggle.classList.contains('active'));
toggleClass(footerContainer, 'hidden', !footerContainerToggle.classList.contains('active'));
toggleRunButton();
resizeFont();
resizeEditor();
timedPreview();
html,
body {
height: 100%;
padding: 0;
margin: 0;
}
body {
display: flex;
flex-direction: column;
}
header,
footer {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
padding: 5px;
}
header {
background: linear-gradient(#FFF, #CCC);
}
footer {
background: linear-gradient(#CCC, #FFF);
}
main {
display: flex;
flex: 1;
}
main.vertical {
flex-direction: column;
}
header *,
footer * {
font: 0.75rem Arial;
color: #333;
}
select,
button,
input {
margin: 0;
}
label[for="editorSizeInput"],
address {
margin-left: auto;
}
#editorSizeInput,
iframe {
padding: 0;
}
output {
font-family: monospace;
}
.toggle {
padding: 2px 6px;
border: 1px solid #666;
border-radius: 12px;
background: transparent;
}
#footerContainerToggle {
width: 16px;
height: 16px;
padding: 0;
border-bottom-width: 5px;
border-radius: 0;
}
.active {
border-color: #333;
background: #FFF;
}
#copyButton {
padding: 0;
border: 0;
background: transparent;
cursor: pointer;
}
img {
display: block;
}
div {
position: relative;
}
#previewerWrapper {
border-left: 5px solid #CCC;
}
main.vertical #previewerWrapper {
border-left: 0;
border-top: 5px solid #CCC;
}
div * {
position: absolute;
width: 100%;
height: 100%;
border: 0;
margin: 0;
background: #FFF;
}
textarea {
box-sizing: border-box;
padding: 5px;
outline: 0;
resize: none;
color: #333;
}
textarea.dark {
background: #333;
color: #FFF;
}
.hidden {
display: none;
}
<header>
<a href="" download="template.html" id="downloadLink" title="Download HTML document">Download</a>
<label for="fontSizeSelector">Font size</label>
<select id="fontSizeSelector">
<option>12</option>
<option>13</option>
<option selected>14</option>
<option>15</option>
<option>16</option>
<option>17</option>
<option>18</option>
<option>19</option>
<option>20</option>
</select>
<label for="previewSelector">Preview</label>
<select id="previewSelector">
<option>Instant</option>
<optgroup label="Delayed">
<option value="500" selected>0.5 s</option>
<option value="1000">1 s</option>
<option value="1500">1.5 s</option>
<option value="2000">2 s</option>
</optgroup>
<option>Manual</option>
</select>
<button type="button" id="runButton">Run</button>
<button type="button" id="resetButton">Reset</button>
<button type="button" id="selectButton">Select</button>
<input type="file" accept="text/html" id="fileInput">
<label for="editorSizeInput">Editor size</label>
<input type="range" id="editorSizeInput">
<output for="editorSizeInput"></output>
<button type="button" class="toggle" id="verticalViewToggle">Vertical view</button>
<button type="button" class="toggle" id="darkEditorToggle">Dark editor</button>
<button type="button" class="toggle" id="spellcheckToggle">Spellcheck</button>
<button type="button" class="toggle" id="footerContainerToggle" title="Toggle footer"></button>
</header>
<main>
<div id="editorWrapper">
<textarea><!doctype html>
<html lang="en">
<head>
<title>HTML Document Template</title>
<style>
p {
font-family: Arial;
}
</style>
</head>
<body>
<p>Hello, world!</p>
<script>
console.log(document.querySelector('p').textContent);
</script>
</body>
</html></textarea>
</div>
<div id="previewerWrapper">
<iframe></iframe>
</div>
</main>
<footer>
<span>Share</span>
<a href="https://x.com/intent/post?text=HTML%20Editor%3A%20online%20HTML%20editor%20with%20real-time%20preview&url=https%3A%2F%2Fhtmleditor.gitlab.io" target="_blank"><img src="images/x.svg" width="16" height="16" alt="X"></a>
<a href="https://www.facebook.com/sharer.php?u=https%3A%2F%2Fhtmleditor.gitlab.io&t=HTML%20Editor%3A%20online%20HTML%20editor%20with%20real-time%20preview" target="_blank"><img src="images/facebook.svg" width="16" height="16" alt="Facebook"></a>
<a href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fhtmleditor.gitlab.io" target="_blank"><img src="images/linkedin.svg" width="16" height="16" alt="LinkedIn"></a>
<a href="mailto:?subject=HTML%20Editor%3A%20online%20HTML%20editor%20with%20real-time%20preview&body=https%3A%2F%2Fhtmleditor.gitlab.io" target="_blank"><img src="images/email.svg" width="16" height="16" alt="Email"></a>
<button type="button" id="copyButton"><img src="images/link.svg" width="16" height="16" alt="Link"></button>
<span id="copyNotification" class="hidden">Copied!</span>
<address><a href="https://codereview.stackexchange.com/questions/293330/html-editor-online-html-editor-with-real-time-preview" title="Code Review Stack Exchange">Feedback</a> | Created by <a href="https://mori.pages.dev" rel="author">Mori</a></address>
</footer>