Description editor: preserve newlines on export, toggle Quote, highlight active toolbar buttons
Template tests / tests (push) Successful in 1m49s
Template tests / tests (pull_request) Successful in 1m38s

- Enter now starts a new <p> (defaultParagraphSeparator) so line breaks
  in the description box show up as line breaks in HTML/Markdown/Confluence
  exports; htmlToMarkdown also handles <div>-separated paragraphs.
- Clicking Quote while already in a blockquote toggles back to a paragraph.
- Toolbar buttons (Bold, Italic, Bullet, Number, Quote) turn blue to
  reflect the formatting state at the current cursor/selection.
This commit is contained in:
2026-06-13 21:08:31 -05:00
parent 46d12cc829
commit bb959a4bb9
3 changed files with 49 additions and 3 deletions
+42 -3
View File
@@ -329,10 +329,32 @@ class GuideEditor {
}
toolbarBtn(action, label, block = null) {
return el('button', {
const btn = el('button', {
type: 'button',
onClick: () => this.formatDescription(action, block),
}, label);
btn.dataset.action = action;
if (block) btn.dataset.block = block;
return btn;
}
/** Reflect the current selection's formatting state on the toolbar buttons. */
updateToolbarState() {
if (!this.dom.richToolbar) return;
for (const btn of this.dom.richToolbar.children) {
const { action, block } = btn.dataset;
let active = false;
try {
if (action === 'formatBlock') {
active = document.queryCommandValue('formatBlock').toLowerCase() === (block || 'blockquote');
} else if (action === 'bold' || action === 'italic' || action === 'insertUnorderedList' || action === 'insertOrderedList') {
active = document.queryCommandState(action);
}
} catch {
active = false;
}
btn.classList.toggle('active', active);
}
}
bindShellEvents() {
@@ -422,7 +444,14 @@ class GuideEditor {
this.dom.addTableBlockBtn.addEventListener('click', () => this.addBlock('table'));
this.dom.descEditor.addEventListener('focus', () => {
// Make Enter start a new paragraph (<p>) rather than a plain <div>,
// so line breaks survive sanitization and show up in exports.
document.execCommand('defaultParagraphSeparator', false, 'p');
if (this.currentStep) this.pushCanvasHistory('description');
this.updateToolbarState();
});
this.dom.descEditor.addEventListener('blur', () => {
for (const btn of this.dom.richToolbar.children) btn.classList.remove('active');
});
this.dom.descEditor.addEventListener('input', () => {
if (!this.currentStep) return;
@@ -430,6 +459,12 @@ class GuideEditor {
this.pendingSave = true;
this.saveStepDebounced();
this.emitMeta();
this.updateToolbarState();
});
this.dom.descEditor.addEventListener('keyup', () => this.updateToolbarState());
this.dom.descEditor.addEventListener('mouseup', () => this.updateToolbarState());
document.addEventListener('selectionchange', () => {
if (document.activeElement === this.dom.descEditor) this.updateToolbarState();
});
this.dom.descEditor.addEventListener('paste', (e) => {
@@ -1821,9 +1856,12 @@ class GuideEditor {
case 'insertOrderedList':
document.execCommand('insertOrderedList');
break;
case 'formatBlock':
document.execCommand('formatBlock', false, block || 'blockquote');
case 'formatBlock': {
const want = block || 'blockquote';
const current = document.queryCommandValue('formatBlock').toLowerCase();
document.execCommand('formatBlock', false, current === want ? 'p' : want);
break;
}
case 'createLink': {
const selectedText = window.getSelection().toString();
const text = selectedText || window.prompt('Link text');
@@ -1844,6 +1882,7 @@ class GuideEditor {
this.pendingSave = true;
this.saveStepDebounced();
}
this.updateToolbarState();
}
onDocumentKeyDown(e) {
+5
View File
@@ -487,6 +487,11 @@ kbd {
margin-bottom: 8px;
}
.rich-toolbar button { padding: 4px 8px; font-size: 12px; }
.rich-toolbar button.active {
background: var(--accent);
border-color: var(--accent);
color: var(--accent-fg);
}
.rich-editor {
min-height: 110px;
max-height: 220px;
+2
View File
@@ -42,6 +42,8 @@ function htmlToMarkdown(html) {
.replace(/<hr\s*\/?>/gi, '\n---\n')
.replace(/<p>/gi, '\n')
.replace(/<\/p>/gi, '\n')
.replace(/<div>/gi, '\n')
.replace(/<\/div>/gi, '\n')
.replace(/<[^>]+>/g, '');
return decodeEntities(out).replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();