feat(workspace): add formatting shortcuts for bold, italic, and links in outliner

This commit is contained in:
sandrodach committed 2026-10-30 16:22:55 +01:00
1 parent 1d6863824b
commit a317ce0fee
4 files changed
+285

No files matched your search

@@ -440,6 +440,33 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
flushDebounce(newContent);
};
const applyFormattingChange = (newVal: string, selStart: number, selEnd: number) => {
setLocalContent(newVal);
localContentRef.current = newVal;
if (debounceTimerRef.current !== null) {
window.clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = window.setTimeout(() => {
debounceTimerRef.current = null;
onUpdateBlockRef.current({
...latestBlockRef.current,
content: newVal,
});
}, 150);
const ta = textareaRef.current;
if (ta) {
ta.value = newVal;
ta.setSelectionRange(selStart, selEnd);
}
setTimeout(() => {
if (textareaRef.current) {
textareaRef.current.setSelectionRange(selStart, selEnd);
adjustHeight();
}
}, 0);
};
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (!canEdit) return;
let val = e.target.value;
@@ -616,6 +643,108 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
}
if (!canEdit) return;
// Formatting shortcuts (Ctrl/Cmd+B, Ctrl/Cmd+I, Ctrl/Cmd+K) - not in code blocks
const isMod = (e.ctrlKey || e.metaKey) && !e.altKey;
if (isMod && block.type !== 'code' && ['b', 'i', 'k'].includes(e.key.toLowerCase())) {
e.preventDefault();
e.stopPropagation();
const ta = textareaRef.current || e.currentTarget;
const start = ta.selectionStart ?? 0;
const end = ta.selectionEnd ?? 0;
const text = ta.value;
const hasSelection = start < end;
if (e.key.toLowerCase() === 'k') {
if (!hasSelection) {
setShowLinkModal(true);
return;
}
const selected = text.slice(start, end);
const placeholder = '…';
const before = text.slice(0, start);
const after = text.slice(end);
const inserted = `[${selected}](${placeholder})`;
const newVal = before + inserted + after;
const selStart = start + 1 + selected.length + 2;
const selEnd = selStart + placeholder.length;
applyFormattingChange(newVal, selStart, selEnd);
return;
}
const isBold = e.key.toLowerCase() === 'b';
const delim = isBold ? '**' : '*';
if (!hasSelection) {
const pair = isBold ? '****' : '**';
const newVal = text.slice(0, start) + pair + text.slice(start);
const newPos = start + delim.length;
applyFormattingChange(newVal, newPos, newPos);
return;
}
const selected = text.slice(start, end);
if (isBold) {
const isOutside =
start >= 2 &&
end + 2 <= text.length &&
text.slice(start - 2, start) === '**' &&
text.slice(end, end + 2) === '**';
const isInside =
selected.length >= 4 &&
selected.startsWith('**') &&
selected.endsWith('**');
if (isOutside) {
const newVal = text.slice(0, start - 2) + selected + text.slice(end + 2);
applyFormattingChange(newVal, start - 2, end - 2);
return;
}
if (isInside) {
const unwrapped = selected.slice(2, -2);
const newVal = text.slice(0, start) + unwrapped + text.slice(end);
applyFormattingChange(newVal, start, start + unwrapped.length);
return;
}
const newVal = text.slice(0, start) + '**' + selected + '**' + text.slice(end);
applyFormattingChange(newVal, start + 2, end + 2);
return;
} else {
const isOutside =
start >= 1 &&
end + 1 <= text.length &&
text[start - 1] === '*' &&
text[end] === '*' &&
(start < 2 || text[start - 2] !== '*') &&
(end + 1 >= text.length || text[end + 1] !== '*');
const isInside =
selected.length >= 2 &&
selected.startsWith('*') &&
selected.endsWith('*') &&
(!selected.startsWith('**') || !selected.endsWith('**') || selected.length < 4);
if (isOutside) {
const newVal = text.slice(0, start - 1) + selected + text.slice(end + 1);
applyFormattingChange(newVal, start - 1, end - 1);
return;
}
if (isInside) {
const unwrapped = selected.slice(1, -1);
const newVal = text.slice(0, start) + unwrapped + text.slice(end);
applyFormattingChange(newVal, start, start + unwrapped.length);
return;
}
const newVal = text.slice(0, start) + '*' + selected + '*' + text.slice(end);
applyFormattingChange(newVal, start + 1, end + 1);
return;
}
}
// Slash command navigation
if (slashSearch !== null) {
if (e.key === 'Escape') {