Add rich text editor to assignment body field

Replaces the plain textarea with a contenteditable div + full toolbar
(bold, italic, underline, H2/H3, paragraph, bullet/numbered lists, link,
YouTube embed) — same as the lesson editor.

Toolbar buttons use mousedown+preventDefault so selection is preserved
when clicking format buttons. Both editors share the same button wiring
via closest('.rte-group') rather than hardcoded element IDs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-07-30 12:16:39 -04:00
1 parent c7dd48d0c3
commit 35ae40819b
2 files changed
+72 -53

No files matched your search

+23 -6
View File
@@ -181,7 +181,7 @@
<p id="import-status" class="hint"></p> <p id="import-status" class="hint"></p>
</div> </div>
<div id="ie-field-lesson" class="ie-field"> <div id="ie-field-lesson" class="ie-field rte-group">
<div class="rte-label">Lesson content</div> <div class="rte-label">Lesson content</div>
<div class="rte-toolbar"> <div class="rte-toolbar">
<button type="button" class="rte-btn" data-cmd="bold" title="Bold"><b>B</b></button> <button type="button" class="rte-btn" data-cmd="bold" title="Bold"><b>B</b></button>
@@ -195,16 +195,33 @@
<button type="button" class="rte-btn" data-cmd="insertUnorderedList" title="Bullet list">• List</button> <button type="button" class="rte-btn" data-cmd="insertUnorderedList" title="Bullet list">• List</button>
<button type="button" class="rte-btn" data-cmd="insertOrderedList" title="Numbered list">1. List</button> <button type="button" class="rte-btn" data-cmd="insertOrderedList" title="Numbered list">1. List</button>
<span class="rte-sep"></span> <span class="rte-sep"></span>
<button type="button" class="rte-btn" id="rte-link-btn" title="Add link">🔗 Link</button> <button type="button" class="rte-btn rte-link-btn" title="Add link">🔗 Link</button>
<button type="button" class="rte-btn" id="rte-youtube-btn" title="Embed YouTube">▶ YouTube</button> <button type="button" class="rte-btn rte-youtube-btn" title="Embed YouTube">▶ YouTube</button>
</div> </div>
<div id="ie-body-lesson" class="rte-editor lesson-body" contenteditable="true" <div id="ie-body-lesson" class="rte-editor lesson-body" contenteditable="true"
data-placeholder="Weather is what the sky and air are doing right now.&#10;Add images, YouTube embeds, and more."></div> data-placeholder="Weather is what the sky and air are doing right now.&#10;Add images, YouTube embeds, and more."></div>
</div> </div>
<div id="ie-field-assignment" class="ie-field"> <div id="ie-field-assignment" class="ie-field rte-group">
<label>Instructions <textarea id="ie-body-assignment" rows="5" placeholder="Draw today's weather and write one sentence."></textarea></label> <div class="rte-label">Instructions</div>
<label>Points possible <input id="ie-points" type="number" min="0" value="10"></label> <div class="rte-toolbar">
<button type="button" class="rte-btn" data-cmd="bold" title="Bold"><b>B</b></button>
<button type="button" class="rte-btn" data-cmd="italic" title="Italic"><i>I</i></button>
<button type="button" class="rte-btn" data-cmd="underline" title="Underline"><u>U</u></button>
<span class="rte-sep"></span>
<button type="button" class="rte-btn" data-cmd="formatBlock" data-val="h2" title="Heading">H2</button>
<button type="button" class="rte-btn" data-cmd="formatBlock" data-val="h3" title="Sub-heading">H3</button>
<button type="button" class="rte-btn" data-cmd="formatBlock" data-val="p" title="Paragraph"></button>
<span class="rte-sep"></span>
<button type="button" class="rte-btn" data-cmd="insertUnorderedList" title="Bullet list">• List</button>
<button type="button" class="rte-btn" data-cmd="insertOrderedList" title="Numbered list">1. List</button>
<span class="rte-sep"></span>
<button type="button" class="rte-btn rte-link-btn" title="Add link">🔗 Link</button>
<button type="button" class="rte-btn rte-youtube-btn" title="Embed YouTube">▶ YouTube</button>
</div>
<div id="ie-body-assignment" class="rte-editor lesson-body" contenteditable="true"
data-placeholder="Draw today's weather and write one sentence."></div>
<label style="margin-top:.75rem">Points possible <input id="ie-points" type="number" min="0" value="10"></label>
</div> </div>
<div id="ie-field-quiz" class="ie-field"> <div id="ie-field-quiz" class="ie-field">
+49 -47
View File
@@ -395,7 +395,7 @@ async function openItemEditor(unitId, itemId) {
$('#ie-type').value = item.type; $('#ie-type').value = item.type;
$('#ie-title').value = item.title; $('#ie-title').value = item.title;
$('#ie-body-lesson').innerHTML = item.body || ''; $('#ie-body-lesson').innerHTML = item.body || '';
$('#ie-body-assignment').value = item.body; $('#ie-body-assignment').innerHTML = item.body || '';
$('#ie-points').value = item.points || 10; $('#ie-points').value = item.points || 10;
$('#ie-due-date').value = item.due_date || ''; $('#ie-due-date').value = item.due_date || '';
$('#ie-allow-retakes').checked = !!item.allow_retakes; $('#ie-allow-retakes').checked = !!item.allow_retakes;
@@ -417,7 +417,7 @@ async function openItemEditor(unitId, itemId) {
$('#ie-type').value = 'lesson'; $('#ie-type').value = 'lesson';
$('#ie-title').value = ''; $('#ie-title').value = '';
$('#ie-body-lesson').innerHTML = ''; $('#ie-body-lesson').innerHTML = '';
$('#ie-body-assignment').value = ''; $('#ie-body-assignment').innerHTML = '';
$('#ie-points').value = 10; $('#ie-points').value = 10;
$('#ie-due-date').value = ''; $('#ie-due-date').value = '';
$('#ie-allow-retakes').checked = false; $('#ie-allow-retakes').checked = false;
@@ -446,57 +446,60 @@ $('#ie-type').addEventListener('change', updateItemFieldVisibility);
// ---------- rich text editor (lesson body) ---------- // ---------- rich text editor (lesson body) ----------
function getLessonBody() { function getRteBody(el) {
const html = $('#ie-body-lesson').innerHTML.trim(); const html = el.innerHTML.trim();
return html === '<br>' ? '' : html; return html === '<br>' ? '' : html;
} }
function getLessonBody() { return getRteBody($('#ie-body-lesson')); }
function getAssignmentBody() { return getRteBody($('#ie-body-assignment')); }
let rtesavedRange = null; // Format buttons — mousedown+preventDefault keeps focus/selection in the editor
// Save selection whenever the editor loses focus (e.g. when clicking a toolbar button)
$('#ie-body-lesson').addEventListener('blur', () => {
const sel = window.getSelection();
rtesavedRange = sel.rangeCount > 0 ? sel.getRangeAt(0).cloneRange() : null;
});
function rteRestoreAndExec(cmd, val = null) {
$('#ie-body-lesson').focus();
if (rtesavedRange) {
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(rtesavedRange);
}
document.execCommand(cmd, false, val);
}
// Format buttons (bold, italic, heading, lists, etc.)
document.querySelectorAll('.rte-btn[data-cmd]').forEach((btn) => { document.querySelectorAll('.rte-btn[data-cmd]').forEach((btn) => {
btn.addEventListener('click', () => { btn.addEventListener('mousedown', (e) => {
rteRestoreAndExec(btn.dataset.cmd, btn.dataset.val || null); e.preventDefault();
const editor = btn.closest('.rte-group').querySelector('.rte-editor');
editor.focus();
document.execCommand(btn.dataset.cmd, false, btn.dataset.val || null);
}); });
}); });
// Link button // Link buttons
$('#rte-link-btn').addEventListener('click', () => { document.querySelectorAll('.rte-link-btn').forEach((btn) => {
const savedRange = rtesavedRange; btn.addEventListener('mousedown', (e) => {
const url = prompt('Enter URL (e.g. https://example.com):'); e.preventDefault();
if (url) { const editor = btn.closest('.rte-group').querySelector('.rte-editor');
rteRestoreAndExec('createLink', url); editor.focus();
} const sel = window.getSelection();
const savedRange = sel.rangeCount > 0 ? sel.getRangeAt(0).cloneRange() : null;
const url = prompt('Enter URL (e.g. https://example.com):');
if (url) {
editor.focus();
if (savedRange) { sel.removeAllRanges(); sel.addRange(savedRange); }
document.execCommand('createLink', false, url);
}
});
}); });
// YouTube embed button // YouTube embed buttons
$('#rte-youtube-btn').addEventListener('click', () => { document.querySelectorAll('.rte-youtube-btn').forEach((btn) => {
const input = prompt('YouTube URL or video ID:'); btn.addEventListener('mousedown', (e) => {
if (!input) return; e.preventDefault();
const match = input.match(/(?:youtu\.be\/|youtube\.com\/(?:embed\/|watch\?v=))([a-zA-Z0-9_-]{11})/); const editor = btn.closest('.rte-group').querySelector('.rte-editor');
const id = match ? match[1] : input.trim(); editor.focus();
if (id) { const sel = window.getSelection();
rteRestoreAndExec( const savedRange = sel.rangeCount > 0 ? sel.getRangeAt(0).cloneRange() : null;
'insertHTML', const input = prompt('YouTube URL or video ID:');
`<br><iframe width="560" height="315" src="https://www.youtube.com/embed/${id}" frameborder="0" allowfullscreen style="max-width:100%;border-radius:10px"></iframe><br>` if (!input) return;
); const match = input.match(/(?:youtu\.be\/|youtube\.com\/(?:embed\/|watch\?v=))([a-zA-Z0-9_-]{11})/);
} const id = match ? match[1] : input.trim();
if (id) {
editor.focus();
if (savedRange) { sel.removeAllRanges(); sel.addRange(savedRange); }
document.execCommand('insertHTML', false,
`<br><iframe width="560" height="315" src="https://www.youtube.com/embed/${id}" frameborder="0" allowfullscreen style="max-width:100%;border-radius:10px"></iframe><br>`
);
}
});
}); });
// ---------- scan a page (photo -> lesson/quiz content via local Ollama) ---------- // ---------- scan a page (photo -> lesson/quiz content via local Ollama) ----------
@@ -624,8 +627,7 @@ $('#import-go-btn').addEventListener('click', async () => {
? `<details><summary>📄 ${result.originalName}</summary><iframe src="${result.htmlUrl}" style="width:100%;border:1px solid #ccc;border-radius:4px;margin-top:.5em;display:block;" onload="try{this.style.height=this.contentDocument.documentElement.scrollHeight+32+'px'}catch(e){this.style.height='500px'}"></iframe><p><a href="${result.url}" download="${result.originalName}">⬇️ Download ${result.originalName}</a></p></details>` ? `<details><summary>📄 ${result.originalName}</summary><iframe src="${result.htmlUrl}" style="width:100%;border:1px solid #ccc;border-radius:4px;margin-top:.5em;display:block;" onload="try{this.style.height=this.contentDocument.documentElement.scrollHeight+32+'px'}catch(e){this.style.height='500px'}"></iframe><p><a href="${result.url}" download="${result.originalName}">⬇️ Download ${result.originalName}</a></p></details>`
: `<a href="${result.url}" target="_blank">📄 ${result.originalName}</a>`; : `<a href="${result.url}" target="_blank">📄 ${result.originalName}</a>`;
if (type === 'assignment') { if (type === 'assignment') {
const cur = $('#ie-body-assignment').value; $('#ie-body-assignment').innerHTML += embed;
$('#ie-body-assignment').value = cur ? cur + '\n\n' + embed : embed;
} else { } else {
$('#ie-body-lesson').innerHTML += embed; $('#ie-body-lesson').innerHTML += embed;
} }
@@ -691,7 +693,7 @@ $('#item-form').addEventListener('submit', async (e) => {
unitId: Number($('#ie-unit-id').value), unitId: Number($('#ie-unit-id').value),
type, type,
title, title,
body: type === 'lesson' ? getLessonBody() : type === 'assignment' ? $('#ie-body-assignment').value : '', body: type === 'lesson' ? getLessonBody() : type === 'assignment' ? getAssignmentBody() : '',
points: type === 'assignment' ? Number($('#ie-points').value) || 0 : 0, points: type === 'assignment' ? Number($('#ie-points').value) || 0 : 0,
refId: type === 'spelling_practice' || type === 'spelling_test' ? Number($('#ie-list').value) refId: type === 'spelling_practice' || type === 'spelling_test' ? Number($('#ie-list').value)
: type === 'flashcards' ? Number($('#ie-deck').value) : null, : type === 'flashcards' ? Number($('#ie-deck').value) : null,