Skip to content

della119/html-text-editor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 

Repository files navigation

HTML Text Editor

A single-file, zero-dependency tool for editing the text in any HTML file — no code, no build step, no server. Drop a file in, click any text and retype it, hit Save. Layout, styling, and images stay exactly as they were.

It's one .html file. Open it in a browser and it works. Nothing is uploaded anywhere — your file never leaves your machine.

What it's for

You receive an HTML deliverable (a report, a slide deck, a one-pager) and need to change a few words, a title, or some numbers — without touching the design or asking a developer. This does exactly that, and nothing more.

  • Edits text — headings, paragraphs, list items, table cells, SVG labels.
  • Doesn't touch layout, colors, images, or structure (that's the safety — you can't accidentally break the design).
  • 🔒 Safe to open anything — scripts in the loaded file are sandboxed and never run; the file is never uploaded.

How to use

  1. Open html-text-editor.html in any modern browser.
  2. Drag an .html file onto the page (or click to browse).
  3. Click any text and edit it. Ctrl+Z / Cmd+Z to undo.
  4. Click Save HTML to download the edited file.

Examples

The examples/ folder has files to try the editor on:

  • examples/ppt-test-cases/ — three full PowerPoint-style decks (a corporate pitch, an editorial deck, a dark keynote) to practice editing slide titles, bullets, numbers, and table cells.
  • examples/editor-test-cases/ — seven structural stress-tests (nested grids, merged-cell tables, Unicode/RTL, SVG text, absolute-positioned slides, forms, and the i18n data-* edge case). Each README.md explains what it tests and what to expect.

Part 1 — The logic, in detail

The one-line idea

The whole thing rests on a single browser feature: document.designMode = 'on'. That turns an entire HTML document into a live word-processor — every piece of text becomes editable in place, with native undo/redo, cursor, and selection — without you writing any per-element editing code. Everything else in the file is just plumbing around that one switch: load a file in, flip the switch, and serialize the result back out.

Here are the 5 mechanisms that make it work.

1. Isolation: load the file into a sandboxed <iframe>

<iframe id="editor-frame" sandbox="allow-same-origin"></iframe>

The uploaded HTML is not injected into the editor's own page — it's loaded into an iframe. Two reasons this matters:

  • The loaded file's CSS can't break the toolbar, and the toolbar's CSS can't distort the file. They live in separate documents.
  • sandbox neutralizes <script> tags in the uploaded file. In a sandbox without allow-scripts, that code simply never runs. So you can safely open any HTML — even something off the internet — and it can't execute, pop alerts, or phone home.

The file content is handed to the iframe via srcdoc (an attribute that takes a raw HTML string):

frame.srcdoc = content;   // content = the entire .html file as text

2. Reading the file: FileReader, no server needed

This whole tool is one static .html file. There is no backend — it never uploads your file anywhere. Reading happens entirely in the browser:

function readFile(file) {
  if (!file || !/\.html?$/i.test(file.name)) {        // only .html / .htm
    alert('Please drop an .html or .htm file.'); return;
  }
  var reader = new FileReader();
  reader.onload = function (e) { loadHTML(e.target.result, file.name); };
  reader.readAsText(file, 'UTF-8');                   // ← UTF-8 is critical
}

The 'UTF-8' argument is what keeps multibyte content (中文 / Arabic / emoji) from turning into mojibake — it tells the browser to decode the bytes as UTF-8 rather than guessing.

3. The magic switch: designMode (fires after the iframe loads)

frame.onload = function () {
  var doc = frame.contentDocument;     // the document INSIDE the iframe
  doc.designMode = 'on';               // ← the entire trick

  // inject a tiny helper stylesheet so the user can see what they're editing
  var style = doc.createElement('style');
  style.id = '__editor_style__';       // tagged so we can remove it on save
  style.textContent = [
    '*:focus { outline: 2px solid rgba(90,160,0,0.5) !important; }',  // green focus ring
    'img { pointer-events:none !important; max-width:100%; }'         // don't drag/resize images
  ].join('\n');
  doc.head.appendChild(style);
};
frame.srcdoc = content;   // set AFTER assigning onload, so onload reliably fires

Two subtle but important details here:

  • Order matters: onload is assigned before srcdoc. If you set srcdoc first, the load can finish before the handler is attached and you'd miss it.
  • The injected <style> is given an id (__editor_style__). That's deliberate — at save time we strip it back out so it doesn't pollute the user's file. The green focus outline is an editing aid, not part of their document.

Why designMode instead of putting contenteditable="true" on each element? Because designMode is universal — it makes the whole document editable in one line, so the tool works on any HTML you throw at it. The contenteditable-per-element approach needs a hand-picked list of selectors, which would make it "case by case."

4. Saving: serialize the live DOM back to a complete file

When you edit text, the browser updates the iframe's live DOM. Saving = read that DOM back out as a string and download it. The tricky part is reconstructing a complete, valid file — outerHTML alone drops the <!DOCTYPE>:

saveBtn.addEventListener('click', function () {
  var doc = frame.contentDocument;
  // 1. remove our injected editing style so it's not in the saved file
  var injected = doc.getElementById('__editor_style__');
  if (injected) injected.remove();

  // 2. rebuild the DOCTYPE by hand (outerHTML omits it)
  var doctype = '';
  if (doc.doctype) {
    doctype = '<!DOCTYPE ' + doc.doctype.name + '>\n';
  }

  // 3. DOCTYPE + the entire <html>…</html>
  var html = doctype + doc.documentElement.outerHTML;

  // 4. trigger a download — again, no server involved
  var blob = new Blob([html], { type: 'text/html; charset=utf-8' });
  var url  = URL.createObjectURL(blob);
  var a    = document.createElement('a');
  a.href = url; a.download = currentFilename;
  a.click();
  URL.revokeObjectURL(url);   // free the memory
});

The download is a classic trick: wrap the string in a Blob, make a temporary object URL pointing at it, create an invisible <a download>, and programmatically .click() it. The browser saves it like any normal download. revokeObjectURL then frees the memory.

5. UI state: three views, toggled by CSS classes

There's no framework here — just three regions shown/hidden by adding/removing a .visible class:

  • #dropzone — the initial "drop a file" circle
  • #toolbar — filename + Save / Open-new buttons (appears once a file is loaded)
  • #editor-frame — the iframe itself
  • plus #overlay — the "Drop to load file" banner that appears when you drag a second file over an already-open one

The drag-and-drop uses a dragCounter to handle a well-known browser quirk: dragenter / dragleave fire for every child element you drag over, so a naïve "hide on dragleave" flickers. Counting enters minus leaves and only hiding at zero fixes it:

document.addEventListener('dragenter', function () { dragCounter++; overlay.classList.add('show'); });
document.addEventListener('dragleave', function () { if (--dragCounter <= 0) overlay.classList.remove('show'); });

The Save button color

#save-btn {
  background: #e8f5d0;   /* light green */
  color: #000;           /* black text */
  border-color: #b7d97a;
  font-weight: 700;
}

Black text needs a light background to be visible — an earlier version used white text on near-black, so simply switching the text to black would have made it invisible. The fix is a pair: black text + light-green fill, which also keeps it looking like the primary action.


The one honest limitation

This edits the rendered text. It does not know about text stored in attributes — like data-cn / data-en bilingual pairs in some i18n templates. If a file rebuilds its text from data-* via JavaScript, your visible edit can be overwritten when the language toggles. That's the boundary between "universal" (≈95% of HTML) and "template-specific."

License

MIT

About

Single-file, zero-dependency tool to edit text in any HTML file — no code, no server, nothing uploaded.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages