Add rich text editor for lesson content input
Replaces the plain textarea with a WYSIWYG contenteditable editor so the formatting the parent sees while writing matches what the student sees when reading. Toolbar supports bold, italic, underline, H2/H3, bullet/numbered lists, links, and YouTube embeds. Selection is saved on editor blur and restored before each execCommand so toolbar clicks don't lose the user's selection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ec26b1deb
commit
3dc4ddea4b
3 files changed
+118
-7
No files matched your search
+18
-3
@@ -162,9 +162,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="ie-field-lesson" class="ie-field">
|
<div id="ie-field-lesson" class="ie-field">
|
||||||
<label>Lesson content (text, or paste HTML — images/YouTube embeds allowed)
|
<div class="rte-label">Lesson content</div>
|
||||||
<textarea id="ie-body-lesson" rows="8" placeholder="Weather is what the sky and air are doing right now. <iframe src="https://www.youtube.com/embed/VIDEO_ID"></iframe>"></textarea>
|
<div class="rte-toolbar">
|
||||||
</label>
|
<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" id="rte-link-btn" title="Add link">🔗 Link</button>
|
||||||
|
<button type="button" class="rte-btn" id="rte-youtube-btn" title="Embed YouTube">▶ YouTube</button>
|
||||||
|
</div>
|
||||||
|
<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. 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">
|
||||||
|
|||||||
+59
-4
@@ -337,7 +337,7 @@ async function openItemEditor(unitId, itemId) {
|
|||||||
$('#ie-heading').textContent = `Edit: ${item.title}`;
|
$('#ie-heading').textContent = `Edit: ${item.title}`;
|
||||||
$('#ie-type').value = item.type;
|
$('#ie-type').value = item.type;
|
||||||
$('#ie-title').value = item.title;
|
$('#ie-title').value = item.title;
|
||||||
$('#ie-body-lesson').value = item.body;
|
$('#ie-body-lesson').innerHTML = item.body || '';
|
||||||
$('#ie-body-assignment').value = item.body;
|
$('#ie-body-assignment').value = 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 || '';
|
||||||
@@ -358,7 +358,7 @@ async function openItemEditor(unitId, itemId) {
|
|||||||
$('#ie-heading').textContent = 'New item';
|
$('#ie-heading').textContent = 'New item';
|
||||||
$('#ie-type').value = 'lesson';
|
$('#ie-type').value = 'lesson';
|
||||||
$('#ie-title').value = '';
|
$('#ie-title').value = '';
|
||||||
$('#ie-body-lesson').value = '';
|
$('#ie-body-lesson').innerHTML = '';
|
||||||
$('#ie-body-assignment').value = '';
|
$('#ie-body-assignment').value = '';
|
||||||
$('#ie-points').value = 10;
|
$('#ie-points').value = 10;
|
||||||
$('#ie-due-date').value = '';
|
$('#ie-due-date').value = '';
|
||||||
@@ -386,6 +386,61 @@ function updateItemFieldVisibility() {
|
|||||||
}
|
}
|
||||||
$('#ie-type').addEventListener('change', updateItemFieldVisibility);
|
$('#ie-type').addEventListener('change', updateItemFieldVisibility);
|
||||||
|
|
||||||
|
// ---------- rich text editor (lesson body) ----------
|
||||||
|
|
||||||
|
function getLessonBody() {
|
||||||
|
const html = $('#ie-body-lesson').innerHTML.trim();
|
||||||
|
return html === '<br>' ? '' : html;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rtesavedRange = null;
|
||||||
|
|
||||||
|
// 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) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
rteRestoreAndExec(btn.dataset.cmd, btn.dataset.val || null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Link button
|
||||||
|
$('#rte-link-btn').addEventListener('click', () => {
|
||||||
|
const savedRange = rtesavedRange;
|
||||||
|
const url = prompt('Enter URL (e.g. https://example.com):');
|
||||||
|
if (url) {
|
||||||
|
rteRestoreAndExec('createLink', url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// YouTube embed button
|
||||||
|
$('#rte-youtube-btn').addEventListener('click', () => {
|
||||||
|
const input = prompt('YouTube URL or video ID:');
|
||||||
|
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) {
|
||||||
|
rteRestoreAndExec(
|
||||||
|
'insertHTML',
|
||||||
|
`<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) ----------
|
||||||
|
|
||||||
let scanImageBase64 = null;
|
let scanImageBase64 = null;
|
||||||
@@ -459,7 +514,7 @@ $('#scan-go-btn').addEventListener('click', async () => {
|
|||||||
const providerLabel = result._provider === 'cloud' ? 'cloud API' : 'local Ollama';
|
const providerLabel = result._provider === 'cloud' ? 'cloud API' : 'local Ollama';
|
||||||
const fallbackNote = result._fallback ? ` (primary failed — used ${providerLabel} as fallback: ${result._primaryError})` : ` via ${providerLabel}`;
|
const fallbackNote = result._fallback ? ` (primary failed — used ${providerLabel} as fallback: ${result._primaryError})` : ` via ${providerLabel}`;
|
||||||
if (mode === 'lesson') {
|
if (mode === 'lesson') {
|
||||||
$('#ie-body-lesson').value = result.body || '';
|
$('#ie-body-lesson').innerHTML = result.body || '';
|
||||||
$('#scan-status').textContent = `✅ Scanned${fallbackNote}. Review the text below before saving.`;
|
$('#scan-status').textContent = `✅ Scanned${fallbackNote}. Review the text below before saving.`;
|
||||||
} else {
|
} else {
|
||||||
$('#ie-questions').innerHTML = '';
|
$('#ie-questions').innerHTML = '';
|
||||||
@@ -590,7 +645,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' ? $('#ie-body-lesson').value : type === 'assignment' ? $('#ie-body-assignment').value : '',
|
body: type === 'lesson' ? getLessonBody() : type === 'assignment' ? $('#ie-body-assignment').value : '',
|
||||||
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,
|
||||||
|
|||||||
@@ -483,6 +483,47 @@ td.overdue, .overdue { background: var(--bad-bg) !important; }
|
|||||||
.parent-comment { background: var(--good-bg); border-radius: 10px; padding: 0.75rem 1rem; margin: 0.75rem 0; color: var(--good); }
|
.parent-comment { background: var(--good-bg); border-radius: 10px; padding: 0.75rem 1rem; margin: 0.75rem 0; color: var(--good); }
|
||||||
.status-banner.bad { background: var(--bad-bg); color: var(--bad); border-radius: 10px; padding: 0.75rem 1rem; margin: 0.75rem 0; }
|
.status-banner.bad { background: var(--bad-bg); color: var(--bad); border-radius: 10px; padding: 0.75rem 1rem; margin: 0.75rem 0; }
|
||||||
|
|
||||||
|
/* ---------- rich text editor (lesson input) ---------- */
|
||||||
|
.rte-label {
|
||||||
|
font-weight: 700; font-size: 0.9rem; margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
.rte-toolbar {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
background: #f0ead8;
|
||||||
|
border: 2px solid #d8d2c0;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
}
|
||||||
|
.rte-btn {
|
||||||
|
font-size: 0.85rem; font-family: inherit; font-weight: 600;
|
||||||
|
background: var(--card); color: var(--ink);
|
||||||
|
border: 1.5px solid #c8c2b0; border-radius: 6px;
|
||||||
|
padding: 0.25rem 0.55rem; cursor: pointer; line-height: 1.4;
|
||||||
|
}
|
||||||
|
.rte-btn:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||||
|
.rte-sep {
|
||||||
|
display: inline-block; width: 1px; height: 1.4rem;
|
||||||
|
background: #c8c2b0; margin: 0 0.15rem; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.rte-editor {
|
||||||
|
min-height: 200px; outline: none; cursor: text;
|
||||||
|
border: 2px solid #d8d2c0; border-radius: 0 0 10px 10px;
|
||||||
|
padding: 0.75rem; margin-bottom: 0;
|
||||||
|
font-size: 1.05rem; line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.rte-editor:focus { border-color: var(--accent); }
|
||||||
|
.rte-editor:empty::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: var(--muted); pointer-events: none; white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.rte-editor h2 { font-size: 1.3rem; margin: 0.6rem 0 0.3rem; }
|
||||||
|
.rte-editor h3 { font-size: 1.1rem; margin: 0.5rem 0 0.25rem; }
|
||||||
|
.rte-editor ul, .rte-editor ol { margin: 0.4rem 0 0.4rem 1.5rem; }
|
||||||
|
.rte-editor a { color: var(--accent-dark); }
|
||||||
|
.rte-editor img, .rte-editor iframe { max-width: 100%; border-radius: 10px; }
|
||||||
|
|
||||||
/* ---------- offline 3-state tasks ---------- */
|
/* ---------- offline 3-state tasks ---------- */
|
||||||
.today-row-offline { background: var(--card); border: 1.5px solid #e2dccf; border-radius: 14px; padding: 0.75rem 1rem; margin-bottom: 0.5rem; text-align: left; }
|
.today-row-offline { background: var(--card); border: 1.5px solid #e2dccf; border-radius: 14px; padding: 0.75rem 1rem; margin-bottom: 0.5rem; text-align: left; }
|
||||||
.offline-main { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
|
.offline-main { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
|
||||||
|
|||||||
Reference in new issue
Block a user