feat: hold the campaign preview request until writing settles, since HTML mode runs it from the Edit tab and a designed body is tens of kilobytes to ship on every 250ms pause, and treat a document sitting behind a leading comment as one when deciding whether to adopt a paste as HTML or to open an A/B arm as markup, because a saved email opens with the tool that wrote it before its doctype

This commit is contained in:
Matthew Meszaros
2026-09-09 06:39:23 -07:00
parent 2c1b5d8204
commit 7f2bfa252e
4 changed files with 20 additions and 3 deletions
@@ -120,6 +120,10 @@ export default function EmailContentEditor({
// notes below the editor come from it, and an author writing markup
// is exactly who needs them while they write.
if (tab !== "preview" && !code) return;
// Switching to Preview is a deliberate act and should feel immediate.
// Writing markup is not: the request carries the whole body, which for
// a designed email is tens of kilobytes, so it waits for a real pause.
const settle = tab === "preview" ? 250 : 800;
// Responses can land out of order; only the newest request may paint.
let active = true;
const t = setTimeout(() => {
@@ -140,7 +144,7 @@ export default function EmailContentEditor({
.catch(() => {
if (active) setServerPreview(null);
});
}, 250);
}, settle);
return () => {
active = false;
clearTimeout(t);
+1 -1
View File
@@ -50,7 +50,7 @@ const DOCUMENT_CSS = `
// designed newsletter) must not be nested inside another one: the doctype and
// the <head> would land in the body, and the frame would preview something the
// recipient will never see. Its own <head> gets our shell instead.
const DOCUMENT_ROOT = /^\s*(?:<!doctype\s+html|<html[\s>])/i;
const DOCUMENT_ROOT = /^\s*(?:<!--[\s\S]*?-->\s*)*(?:<!doctype\s+html|<html[\s>])/i;
const HEAD_OPEN = /<head\b[^>]*>/i;
const HTML_OPEN = /<html\b[^>]*>/i;
+10
View File
@@ -34,6 +34,12 @@ describe("detectPastedEmail", () => {
expect(detectPastedEmail(clipboard({ "text/plain": "<table without a close" }))).toBeNull();
});
// A saved email opens with the tool that wrote it before its doctype.
it("adopts a document behind a leading comment", () => {
const doc = "<!-- saved from Mailchimp --><!DOCTYPE html><html><body><p>Hi</p></body></html>";
expect(detectPastedEmail(clipboard({ "text/html": doc }))).toBe(doc);
});
it("is null for an empty clipboard", () => {
expect(detectPastedEmail(null)).toBeNull();
expect(detectPastedEmail(clipboard({}))).toBeNull();
@@ -46,6 +52,10 @@ describe("isDocumentBody", () => {
expect(isDocumentBody('<style>.x{color:red}</style><p class="x">x</p>')).toBe(true);
});
it("sees a document behind a leading comment", () => {
expect(isDocumentBody("<!-- saved from Mailchimp --><!doctype html><html><body>x</body></html>")).toBe(true);
});
it("is false for an ordinary campaign body", () => {
expect(isDocumentBody("<p>Hi {{.FirstName}}, do you have ten minutes?</p>")).toBe(false);
expect(isDocumentBody('<table><tr><td style="padding:8px">Hi</td></tr></table>')).toBe(false);
+4 -1
View File
@@ -14,7 +14,10 @@
// a live selection was copied out of a rendered page, not a file of markup.
const FRAGMENT_MARKER = /<!--\s*StartFragment\s*-->/i;
const DOCUMENT_ROOT = /^\s*(?:<!doctype\s+html|<html[\s>])/i;
// Leading comments are skipped: a saved email opens with the tool that wrote
// it ("<!-- saved from ... -->") before its doctype, and treating that as an
// ordinary fragment would nest one document inside another.
const DOCUMENT_ROOT = /^\s*(?:<!--[\s\S]*?-->\s*)*(?:<!doctype\s+html|<html[\s>])/i;
const STYLE_BLOCK = /<style[\s>]/i;
// Tags that open a paste which is markup someone copied as text: the source of