Skip to content

Quill Editor

Overview

The Quill rich text editor is a common component used across many Live Apps for content editing. This guide covers the recommended setup, known issues, and proven solutions for integrating Quill into PrimeThink Live Apps.

Version

Use Quill 2.0.2 — do NOT use 2.0.3 or later. Version 2.0.3 introduced a regression where getSemanticHTML() converts all spaces to  , breaking word-wrap in view mode. There is no config option to disable this behavior.

<link href="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"></script>

Saving Content

Always strip &nbsp; from getSemanticHTML() output as a safety net, even on 2.0.2:

tab.content = quill.getSemanticHTML().replace(/&nbsp;/g, ' ');

Without this, non-breaking spaces prevent the browser from wrapping text at word boundaries, causing horizontal overflow.

Editor and View Mode Spacing

Quill's .ql-editor has no default paragraph or heading margins. Add CSS to match your view mode styling:

.ql-editor p { margin: 0.5em 0; }
.ql-editor h1 { font-size: 2em; font-weight: bold; margin: 0.67em 0; }
.ql-editor h2 { font-size: 1.5em; font-weight: bold; margin: 0.75em 0; }
.ql-editor h3 { font-size: 1.17em; font-weight: bold; margin: 0.83em 0; }
.ql-editor ul, .ql-editor ol { margin: 0.5em 0; }
.ql-editor li { margin: 0.25em 0; }
.ql-editor blockquote {
    border-left: 4px solid #d1d5db;
    padding-left: 1em;
    margin: 0.5em 0;
    color: #6b7280;
}

Importing Documents

When importing files via pt.uploadFiles() + pt.getDocumentText(), the extracted text has hard line breaks at the original page column width (typically 80-120 characters). This causes words to be split mid-word (e.g., structura\nl becomes structura l).

Fix: Normalize single newlines into spaces before passing to marked.parse(), while preserving paragraph breaks and markdown syntax:

const normalized = text.replace(/([^\n])\n(?!\n|#|\*|-|\d+\.|>|```)/g, '$1 ');
const html = marked.parse(normalized);

This regex joins lines that are part of the same paragraph but preserves:

  • Double newlines (paragraph breaks)
  • Lines starting with # (headings)
  • Lines starting with *, - (lists)
  • Lines starting with 1. etc. (ordered lists)
  • Lines starting with > (blockquotes)
  • Lines starting with ``` (code fences)

Dark Mode

Override Quill's toolbar and container styles for dark mode:

.dark .ql-toolbar.ql-snow {
    border-color: #4b5563;
    background: #1f2937;
}
.dark .ql-toolbar .ql-stroke { stroke: #d1d5db; }
.dark .ql-toolbar .ql-fill { fill: #d1d5db; }
.dark .ql-toolbar .ql-picker-label { color: #d1d5db; }
.dark .ql-toolbar .ql-picker-options { background: #374151; }
.dark .ql-container.ql-snow {
    border-color: #4b5563;
    background: #111827;
    color: #e5e7eb;
}

View Mode Content Styling

Add overflow-wrap: break-word to the view container and white-space: pre-wrap on <pre> blocks to prevent horizontal scroll:

.view-content {
    overflow-wrap: break-word;
    word-wrap: break-word;
}
.view-content pre {
    white-space: pre-wrap;
    overflow-x: auto;
}

Image Handling

Quill stores pasted or inserted images as base64 data: URIs inline. These bloat the entity data. Intercept via the text-change event, upload to PrimeThink, and replace with a hosted URL:

quill.on('text-change', () => {
    clearTimeout(imgTimer);
    imgTimer = setTimeout(uploadBase64Images, 500);
});

async function uploadBase64Images() {
    const imgs = quill.root.querySelectorAll('img[src^="data:"]');
    for (const img of imgs) {
        // Convert base64 to Blob
        const res = await fetch(img.src);
        const blob = await res.blob();

        // Upload via PrimeThink
        const formData = new FormData();
        formData.append('files', blob, 'image.png');
        const result = await pt.uploadFiles(formData);

        // Replace inline base64 with hosted URL
        img.src = result.download_url;
    }
}

Checklist Shortcut

Quill doesn't have a built-in [] to checklist shortcut. Implement via a keydown listener on quill.root:

quill.root.addEventListener('keydown', (e) => {
    if (e.key !== ' ') return;

    const selection = quill.getSelection();
    if (!selection) return;

    const [line] = quill.getLine(selection.index);
    const lineText = line.domNode.textContent;

    if (lineText.startsWith('[]')) {
        e.preventDefault();
        const lineIndex = quill.getIndex(line);
        quill.deleteText(lineIndex, 2);
        quill.formatLine(lineIndex, 1, 'list', 'unchecked');
    } else if (lineText.startsWith('[x]')) {
        e.preventDefault();
        const lineIndex = quill.getIndex(line);
        quill.deleteText(lineIndex, 3);
        quill.formatLine(lineIndex, 1, 'list', 'checked');
    }
});

Key Gotchas

Issue Cause Fix
Text won't wrap in view mode &nbsp; in saved HTML Strip on save + use Quill 2.0.2
Words split mid-word after import Hard line breaks from PDF/DOCX extraction Normalize \n to space before marked.parse()
No spacing between paragraphs in editor Quill has no default margins Add CSS for .ql-editor p/h1/h2/h3
Images bloat entity data Base64 inline images Upload to PrimeThink, replace with URL
Horizontal scroll on code blocks <pre> default white-space: pre Use white-space: pre-wrap