From a321a881d8a653bd45d64edbb8ace219cfff537e Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 24 Jul 2026 15:35:12 -0400 Subject: [PATCH] Jul 24 - Update Rich-text editor to enhance table editor 6 --- .claude/settings.json | 4 +++- CLAUDE.md | 12 ++++++++++-- app.py | 18 ++++++++++-------- static/css/style.css | 4 ++++ templates/admin/topic_form.html | 14 ++++++-------- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index e69a1e1..e3695a2 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -33,7 +33,9 @@ "Bash(curl -sSL https://raw.githubusercontent.com/attoae/quill-table-better/main/src/utils/clipboard-matchers.ts -o cm.ts -w \"cm %{http_code}\\\\n\")", "Bash(curl -sSL https://raw.githubusercontent.com/attoae/quill-table-better/main/src/ui/table-menus.ts -o tm.ts -w \"tm %{http_code}\\\\n\")", "Bash(curl -sSL https://raw.githubusercontent.com/attoae/quill-table-better/main/src/ui/table-properties-form.ts -o tpf.ts -w \"tpf %{http_code}\\\\n\")", - "Bash(python -m py_compile app.py)" + "Bash(python -m py_compile app.py)", + "Bash(curl -sSL https://raw.githubusercontent.com/attoae/quill-table-better/main/src/utils/index.ts -o utils.ts -w \"utils %{http_code}\\\\n\")", + "Bash(grep -nA15 \"function getAlign|getAlign =|export const getAlign\" utils.ts)" ] } } diff --git a/CLAUDE.md b/CLAUDE.md index 12e6bc2..b757914 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -128,10 +128,18 @@ shows only `is_published` topics (sections with no published topics are hidden). load still leaves a working editor + textarea. Registration and keyboard bindings (`QuillTableBetter.keyboardBindings`) live in `topic_form.html`. - **Save/load round-trip (critical):** save with `deleteTableTemporary()` then - `quill.getSemanticHTML()`. **Load must NOT use `dangerouslyPasteHTML`/`setContents`** - — quill-table-better renders tables *blank* that way. Instead convert then apply: + `quill.root.innerHTML` — **NOT `getSemanticHTML()`**, which rewrites Quill's + `ql-align-*` class as an inline `text-align` style; quill-table-better's + `getAlign()` reads only the class, so semantic HTML makes the cell-properties + dialog always show "left". **Load must NOT use `dangerouslyPasteHTML`/`setContents`** + either — that renders tables *blank*. Instead convert then apply: `quill.updateContents(quill.clipboard.convert({html}), 'user')`. The non-table fallback path still uses `dangerouslyPasteHTML`. + - **Alignment is a CSS class, not inline style.** Cell alignment = Quill's default + class-based align (`ql-align-center` on the cell `

`). The sanitizer therefore + allows `class` on p/table tags (also needed for `ql-table-block`), and the public + page styles `.prose .ql-align-*`. Do NOT register `attributors/style/align` — it + breaks the properties-dialog readback described above. - **Sanitize on save:** `sanitize_html()` (bleach) runs on every `body_html` write — `ALLOWED_TAGS`/`ALLOWED_ATTRS` cover `img` + full table tags with `colspan/rowspan/data-*/style`; a `CSSSanitizer` (bleach[css] + `tinycss2`) diff --git a/app.py b/app.py index 60b9b74..971b917 100644 --- a/app.py +++ b/app.py @@ -97,21 +97,23 @@ ALLOWED_TAGS = [ # tables (quill-table-better: resizable cols, aligned/styled cells) "table", "thead", "tbody", "tr", "td", "th", "col", "colgroup", ] -# quill-table-better carries column widths, alignment, borders and background -# as inline `style` on the table/cell tags, plus data-* bookkeeping attributes. +# quill-table-better carries column widths, borders and background as inline +# `style`, plus data-* bookkeeping attributes. Cell alignment is Quill's +# `ql-align-*` CSS class on the cell's

block, and quill-table-better also +# tags blots with `ql-table-block`/`table-th-block` classes — all of which must +# survive so the table (and its alignment) round-trips back into the editor. _TABLE_CELL_ATTRS = [ "data-row", "data-cell", "data-class", "colspan", "rowspan", - "width", "height", "style", + "width", "height", "style", "class", ] ALLOWED_ATTRS = { "a": ["href", "title", "target", "rel"], "img": ["src", "alt", "width", "height"], - # cell alignment lives as inline `text-align` on the cell's

block - "p": ["style"], + "p": ["style", "class", "data-cell", "data-row"], "table": ["class", "style", "align", "width", "height", "data-class"], - "colgroup": ["style"], - "col": ["width", "span", "style"], - "tr": ["data-row", "style"], + "colgroup": ["style", "class"], + "col": ["width", "span", "style", "class"], + "tr": ["data-row", "style", "class"], "td": _TABLE_CELL_ATTRS, "th": _TABLE_CELL_ATTRS, } diff --git a/static/css/style.css b/static/css/style.css index edd8e01..4ec7cfb 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -163,6 +163,10 @@ body{ .prose table{border-collapse:collapse;table-layout:fixed;max-width:100%;margin:1em 0;font-size:.95rem} .prose td,.prose th{border:1px solid var(--hair);padding:.5rem .65rem;text-align:left;vertical-align:top;word-wrap:break-word} .prose th{background:rgba(0,0,0,.03);color:var(--ink);font-weight:600} +/* Cell alignment is Quill's align class on the cell's

(see admin editor). */ +.prose .ql-align-center{text-align:center} +.prose .ql-align-right{text-align:right} +.prose .ql-align-justify{text-align:justify} .topic__link{ align-self:flex-start;display:inline-flex;align-items:center;gap:8px; diff --git a/templates/admin/topic_form.html b/templates/admin/topic_form.html index 3113c11..08fefbf 100644 --- a/templates/admin/topic_form.html +++ b/templates/admin/topic_form.html @@ -149,11 +149,6 @@ var toolbarEl = document.getElementById('editor-toolbar'); var editorEl = document.getElementById('editor'); - // Persist alignment as inline `text-align` (style) rather than a Quill CSS - // class, so cell alignment survives the HTML sanitizer and renders on the - // public page with no extra CSS. Must run before the editor is created. - Quill.register(Quill.import('attributors/style/align'), true); - // quill-table-better: resizable columns/rows + a cell menu for alignment, // borders and background. Registered only if the vendor script loaded. var hasTables = typeof QuillTableBetter !== 'undefined'; @@ -226,12 +221,15 @@ var hasText = quill.getText().trim().length > 0; var hasEmbed = quill.root.querySelector('img, table'); if (!hasText && !hasEmbed) { ta.value = ''; return; } - // Drop quill-table-better's transient selection nodes, then serialize - // clean semantic HTML (not raw innerHTML, which can leak UI markup). + // Drop quill-table-better's transient selection nodes, then save the raw + // editor HTML. We deliberately avoid getSemanticHTML() here: it rewrites + // Quill's `ql-align-*` class as an inline text-align style, which breaks + // quill-table-better's cell-alignment readback (its getAlign() only reads + // the class). innerHTML keeps the class so alignment round-trips. if (hasTables) { try { quill.getModule('table-better').deleteTableTemporary(); } catch (e) {} } - ta.value = quill.getSemanticHTML(); + ta.value = quill.root.innerHTML; }); })();