Description editor: preserve newlines on export, toggle Quote, highlight active toolbar buttons
- 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:
+42
-3
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user