Jul 24 - Update Rich-text editor to support uploading images and inserting table

This commit is contained in:
2026-07-24 09:55:41 -04:00
parent 69b97d51fe
commit 10a8658b67
12 changed files with 227 additions and 968 deletions
+71 -3
View File
@@ -74,8 +74,17 @@
</span>
<span class="ql-formats">
<button class="ql-link"></button>
<button class="ql-image" title="Insert image"></button>
<button class="ql-clean"></button>
</span>
<span class="ql-formats ql-table-tools">
<button type="button" class="ql-table-op" data-op="insert" title="Insert table"></button>
<button type="button" class="ql-table-op" data-op="row" title="Add row below">+Row</button>
<button type="button" class="ql-table-op" data-op="col" title="Add column right">+Col</button>
<button type="button" class="ql-table-op" data-op="delrow" title="Delete row">Row</button>
<button type="button" class="ql-table-op" data-op="delcol" title="Delete column">Col</button>
<button type="button" class="ql-table-op" data-op="deltable" title="Delete table">✕Table</button>
</span>
</div>
<div id="editor"></div>
<!-- Real field. Quill enhances it; if Quill fails to load, this stays a
@@ -140,16 +149,75 @@
(function () {
if (typeof Quill === 'undefined') return; // textarea stays usable
var ta = document.getElementById('body_src');
var toolbar = document.getElementById('editor-toolbar');
var toolbarEl = document.getElementById('editor-toolbar');
var editorEl = document.getElementById('editor');
var quill = new Quill(editorEl, { theme: 'snow', modules: { toolbar: toolbar } });
var quill = new Quill(editorEl, {
theme: 'snow',
// `table: true` enables Quill 2's built-in table module.
modules: { toolbar: toolbarEl, table: true }
});
if (ta.value.trim()) quill.clipboard.dangerouslyPasteHTML(ta.value);
var csrf = document.querySelector('meta[name="csrf-token"]').content;
var tableModule = quill.getModule('table');
// --- Image button: upload the file, then embed the returned URL. This
// keeps the DB small (no base64) and the image is served as a static file.
quill.getModule('toolbar').addHandler('image', function () {
var input = document.createElement('input');
input.type = 'file';
input.accept = 'image/png,image/jpeg,image/gif,image/webp';
input.onchange = function () {
var file = input.files && input.files[0];
if (!file) return;
var fd = new FormData();
fd.append('file', file);
fetch('{{ url_for("admin.upload") }}', {
method: 'POST', headers: { 'X-CSRFToken': csrf }, body: fd
}).then(function (r) {
return r.json().then(function (j) { return { ok: r.ok, body: j }; });
}).then(function (res) {
if (!res.ok) { alert(res.body.error || 'Upload failed.'); return; }
var range = quill.getSelection(true);
quill.insertEmbed(range.index, 'image', res.body.url, 'user');
quill.setSelection(range.index + 1, 'silent');
}).catch(function () { alert('Upload failed.'); });
};
input.click();
});
// --- Table controls. Row/column ops act on the cell holding the cursor.
toolbarEl.querySelectorAll('.ql-table-op').forEach(function (btn) {
// Preventing the mousedown default keeps the editor's selection so the
// table module knows which cell the cursor is in.
btn.addEventListener('mousedown', function (e) { e.preventDefault(); });
btn.addEventListener('click', function () {
var op = btn.getAttribute('data-op');
try {
if (op === 'insert') {
if (!quill.getSelection()) quill.setSelection(quill.getLength() - 1, 0);
tableModule.insertTable(3, 3);
}
else if (op === 'row') tableModule.insertRowBelow();
else if (op === 'col') tableModule.insertColumnRight();
else if (op === 'delrow') tableModule.deleteRow();
else if (op === 'delcol') tableModule.deleteColumn();
else if (op === 'deltable') tableModule.deleteTable();
} catch (err) {
alert('Place the cursor inside a table cell first.');
}
});
});
// Switch the UI from textarea to Quill only once it's ready.
document.body.classList.add('quill-on');
ta.form.addEventListener('submit', function () {
ta.value = quill.getText().trim().length ? quill.root.innerHTML : '';
// getText() is empty for an image/table-only body, so also check for
// embeds before treating the editor as blank (which would wipe the save).
var hasText = quill.getText().trim().length > 0;
var hasEmbed = quill.root.querySelector('img, table');
ta.value = (hasText || hasEmbed) ? quill.root.innerHTML : '';
});
})();
</script>