fix(workspace): enable automatic multi-line text wrapping for note blocks without horizontal scrolling
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 20s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 20s
This commit is contained in:
1 parent
ef8cf8f022
commit
58f02e0927
1 file changed
+151
-320
@@ -183,6 +183,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
setActiveCursorOffset(offset);
|
setActiveCursorOffset(offset);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const autoResizeTextarea = (el: HTMLTextAreaElement | null) => {
|
||||||
|
if (!el) return;
|
||||||
|
el.style.height = 'auto';
|
||||||
|
el.style.height = `${el.scrollHeight}px`;
|
||||||
|
};
|
||||||
|
|
||||||
// Drag and Drop reordering state for blocks
|
// Drag and Drop reordering state for blocks
|
||||||
const [draggingBlockId, setDraggingBlockId] = useState<string | null>(null);
|
const [draggingBlockId, setDraggingBlockId] = useState<string | null>(null);
|
||||||
const [dragOverBlockTarget, setDragOverBlockTarget] = useState<{ blockId: string; position: 'before' | 'after' | 'inside' } | null>(null);
|
const [dragOverBlockTarget, setDragOverBlockTarget] = useState<{ blockId: string; position: 'before' | 'after' | 'inside' } | null>(null);
|
||||||
@@ -2180,9 +2186,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
if (!activeEditingBlockId) return;
|
if (!activeEditingBlockId) return;
|
||||||
const el = blockInputRefs.current[activeEditingBlockId];
|
const el = blockInputRefs.current[activeEditingBlockId];
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const segStartAttr = el.getAttribute('data-seg-start');
|
if (el instanceof HTMLTextAreaElement) {
|
||||||
if (segStartAttr !== null && 'setSelectionRange' in el && typeof el.value === 'string') {
|
autoResizeTextarea(el);
|
||||||
const segStart = parseInt(segStartAttr, 10) || 0;
|
}
|
||||||
|
if ('setSelectionRange' in el && typeof el.value === 'string') {
|
||||||
|
const segStartAttr = el.getAttribute('data-seg-start');
|
||||||
|
const segStart = segStartAttr !== null ? parseInt(segStartAttr, 10) || 0 : 0;
|
||||||
const desiredOffset = activeCursorOffsetRef.current;
|
const desiredOffset = activeCursorOffsetRef.current;
|
||||||
const localPos = Math.max(0, Math.min(el.value.length, desiredOffset - segStart));
|
const localPos = Math.max(0, Math.min(el.value.length, desiredOffset - segStart));
|
||||||
if (document.activeElement !== el) {
|
if (document.activeElement !== el) {
|
||||||
@@ -2257,6 +2266,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const nextCursor = cursor + 1;
|
const nextCursor = cursor + 1;
|
||||||
setBlockCursor(block.id, nextCursor);
|
setBlockCursor(block.id, nextCursor);
|
||||||
updateBlock(block.id, { content: nextContent });
|
updateBlock(block.id, { content: nextContent });
|
||||||
|
if (target instanceof HTMLTextAreaElement) {
|
||||||
|
setTimeout(() => autoResizeTextarea(target), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2269,7 +2281,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
let afterContent = '';
|
let afterContent = '';
|
||||||
let updatedCurrent: { id: string; content: string } | undefined = undefined;
|
let updatedCurrent: { id: string; content: string } | undefined = undefined;
|
||||||
|
|
||||||
if (!fullContent.includes('\n') && target && typeof target.selectionStart === 'number') {
|
if (target && typeof target.selectionStart === 'number') {
|
||||||
const cursor = segStart + target.selectionStart;
|
const cursor = segStart + target.selectionStart;
|
||||||
const before = fullContent.slice(0, cursor);
|
const before = fullContent.slice(0, cursor);
|
||||||
const after = fullContent.slice(cursor);
|
const after = fullContent.slice(cursor);
|
||||||
@@ -2278,7 +2290,6 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
afterContent = after;
|
afterContent = after;
|
||||||
}
|
}
|
||||||
} else if (fullContent.endsWith('\n')) {
|
} else if (fullContent.endsWith('\n')) {
|
||||||
// Clean up trailing empty line if user pressed Shift+Enter then immediately Enter
|
|
||||||
const trimmedTrailing = fullContent.replace(/\n+$/, '');
|
const trimmedTrailing = fullContent.replace(/\n+$/, '');
|
||||||
if (trimmedTrailing !== fullContent) {
|
if (trimmedTrailing !== fullContent) {
|
||||||
updatedCurrent = { id: block.id, content: trimmedTrailing };
|
updatedCurrent = { id: block.id, content: trimmedTrailing };
|
||||||
@@ -2309,90 +2320,61 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
deleteSingleBlock(block.id, true);
|
deleteSingleBlock(block.id, true);
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
segStart > 0 &&
|
|
||||||
target &&
|
target &&
|
||||||
target.selectionStart === 0 &&
|
target.selectionStart === 0 &&
|
||||||
target.selectionEnd === 0
|
target.selectionEnd === 0
|
||||||
) {
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (fullContent[segStart - 1] === '\n') {
|
if ((block.level || 0) > 0) {
|
||||||
// Join current line with previous line within the same bullet
|
outdentBlock(block.id);
|
||||||
const joined = fullContent.slice(0, segStart - 1) + fullContent.slice(segStart);
|
return;
|
||||||
setBlockCursor(block.id, segStart - 1);
|
}
|
||||||
handleContentChange(block, joined);
|
if (selectedDoc && index > 0) {
|
||||||
} else {
|
const prevBlock = selectedDoc.blocks[index - 1];
|
||||||
// Move cursor into preceding inline token to unwrap it
|
if (prevBlock && prevBlock.type !== 'divider') {
|
||||||
setBlockCursor(block.id, segStart - 1);
|
const prevLen = (prevBlock.content || '').length;
|
||||||
|
const mergedContent = (prevBlock.content || '') + fullContent;
|
||||||
|
const nextBlocks = selectedDoc.blocks
|
||||||
|
.map((b) => (b.id === prevBlock.id ? { ...b, content: mergedContent } : b))
|
||||||
|
.filter((b) => b.id !== block.id);
|
||||||
|
persistDocument({ ...selectedDoc, blocks: nextBlocks }, true);
|
||||||
|
focusBlock(prevBlock.id, prevLen);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!e.shiftKey && e.key === 'ArrowLeft') {
|
|
||||||
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
|
||||||
if (segStart > 0 && target && target.selectionStart === 0 && target.selectionEnd === 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
setBlockCursor(block.id, segStart - 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (target && typeof target.selectionStart === 'number') {
|
|
||||||
setBlockCursor(block.id, segStart + target.selectionStart);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (!e.shiftKey && e.key === 'ArrowRight') {
|
|
||||||
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
|
||||||
const fullContent = block.content || '';
|
|
||||||
if (
|
|
||||||
target &&
|
|
||||||
typeof target.selectionStart === 'number' &&
|
|
||||||
target.selectionStart === target.value.length &&
|
|
||||||
segStart + target.value.length < fullContent.length
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
|
||||||
setBlockCursor(block.id, segStart + target.value.length + 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (target && typeof target.selectionStart === 'number') {
|
|
||||||
setBlockCursor(block.id, segStart + target.selectionStart);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (!e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
} else if (!e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
||||||
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
||||||
const fullContent = block.content || '';
|
const fullContent = block.content || '';
|
||||||
if (fullContent.includes('\n') && target && typeof target.selectionStart === 'number') {
|
if (target && typeof target.selectionStart === 'number') {
|
||||||
const cursor = segStart + target.selectionStart;
|
const cursor = target.selectionStart;
|
||||||
const rawLines = fullContent.split('\n');
|
if (e.key === 'ArrowUp') {
|
||||||
let run = 0;
|
const firstNewline = fullContent.indexOf('\n');
|
||||||
const lineMeta = rawLines.map((lt) => {
|
if (firstNewline === -1 || cursor <= firstNewline) {
|
||||||
const start = run;
|
if (index > 0 && selectedDoc) {
|
||||||
const end = start + lt.length;
|
const prev = selectedDoc.blocks[index - 1];
|
||||||
run = end + 1;
|
if (prev) {
|
||||||
return { start, end, len: lt.length };
|
e.preventDefault();
|
||||||
});
|
focusBlock(prev.id);
|
||||||
const curLineIdx = lineMeta.findIndex((m) => cursor >= m.start && cursor <= m.end);
|
return;
|
||||||
if (curLineIdx !== -1) {
|
}
|
||||||
const col = cursor - lineMeta[curLineIdx].start;
|
}
|
||||||
if (e.key === 'ArrowUp' && curLineIdx > 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
const prevLine = lineMeta[curLineIdx - 1];
|
|
||||||
setBlockCursor(block.id, prevLine.start + Math.min(col, prevLine.len));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (e.key === 'ArrowDown' && curLineIdx < lineMeta.length - 1) {
|
} else if (e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
const lastNewline = fullContent.lastIndexOf('\n');
|
||||||
const nextLine = lineMeta[curLineIdx + 1];
|
if (lastNewline === -1 || cursor > lastNewline) {
|
||||||
setBlockCursor(block.id, nextLine.start + Math.min(col, nextLine.len));
|
if (selectedDoc && index < selectedDoc.blocks.length - 1) {
|
||||||
return;
|
const next = selectedDoc.blocks[index + 1];
|
||||||
|
if (next) {
|
||||||
|
e.preventDefault();
|
||||||
|
focusBlock(next.id, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!e.shiftKey && (e.key === 'Home' || e.key === 'End')) {
|
|
||||||
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (target && typeof target.selectionStart === 'number') {
|
|
||||||
setBlockCursor(block.id, segStart + target.selectionStart);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
|
setSlashBlockId(null);
|
||||||
setActiveEditingBlockId(null);
|
setActiveEditingBlockId(null);
|
||||||
(e.target as HTMLElement)?.blur();
|
(e.target as HTMLElement)?.blur();
|
||||||
}
|
}
|
||||||
@@ -2484,6 +2466,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const tryFocus = () => {
|
const tryFocus = () => {
|
||||||
const el = blockInputRefs.current[blockId];
|
const el = blockInputRefs.current[blockId];
|
||||||
if (el) {
|
if (el) {
|
||||||
|
if (el instanceof HTMLTextAreaElement) {
|
||||||
|
autoResizeTextarea(el);
|
||||||
|
}
|
||||||
el.focus();
|
el.focus();
|
||||||
if ('setSelectionRange' in el && typeof (el as any).value === 'string') {
|
if ('setSelectionRange' in el && typeof (el as any).value === 'string') {
|
||||||
const segStartAttr = el.getAttribute('data-seg-start');
|
const segStartAttr = el.getAttribute('data-seg-start');
|
||||||
@@ -3062,8 +3047,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Live inline token-aware editor: splits multi-line bullets (Shift+Enter) into visual rows and renders
|
// Auto-resizing textarea / formatted view for bullet points and tasks:
|
||||||
// completed #hashtags and links inline as pills on each line while unwrapping any token touched by the cursor.
|
// - When editing: renders an auto-expanding textarea that naturally wraps text (no horizontal scrolling).
|
||||||
|
// - When inactive: renders full rich tokens (Wiki-Links, external URLs, #hashtags) with word wrapping.
|
||||||
const renderLiveInlineBlock = (
|
const renderLiveInlineBlock = (
|
||||||
block: DocumentBlock,
|
block: DocumentBlock,
|
||||||
originalIndex: number,
|
originalIndex: number,
|
||||||
@@ -3072,240 +3058,59 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const text = block.content || '';
|
const text = block.content || '';
|
||||||
if (isReadOnly) {
|
if (isReadOnly) {
|
||||||
return (
|
return (
|
||||||
<div className={`w-full text-sm leading-relaxed break-words min-h-[22px] cursor-default ${extraTextClass}`}>
|
<div className={`w-full text-sm leading-relaxed break-words whitespace-pre-wrap min-h-[22px] cursor-default ${extraTextClass}`}>
|
||||||
{renderFormattedText(text, block.id)}
|
{renderFormattedText(text, block.id)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEditing = activeEditingBlockId === block.id;
|
const isEditing = activeEditingBlockId === block.id;
|
||||||
const cursor = isEditing ? Math.max(0, Math.min(text.length, activeCursorOffset)) : text.length;
|
|
||||||
|
|
||||||
const rawLines = text.split('\n');
|
if (!isEditing) {
|
||||||
let runningOffset = 0;
|
return (
|
||||||
const lines = rawLines.map((lineText, lineIdx) => {
|
<div
|
||||||
const lineStart = runningOffset;
|
onClick={(e) => {
|
||||||
const lineEnd = lineStart + lineText.length;
|
if (isReadOnly) return;
|
||||||
runningOffset = lineEnd + 1; // +1 for the '\n' delimiter
|
if ((e.target as HTMLElement).closest('button, a')) return;
|
||||||
return { lineIdx, lineText, lineStart, lineEnd };
|
focusBlock(block.id);
|
||||||
});
|
}}
|
||||||
|
className={`w-full text-sm leading-relaxed break-words whitespace-pre-wrap min-h-[22px] cursor-text ${extraTextClass}`}
|
||||||
let activeLineIdx = lines.findIndex((l) => cursor >= l.lineStart && cursor <= l.lineEnd);
|
>
|
||||||
if (activeLineIdx === -1) {
|
{text ? (
|
||||||
activeLineIdx = lines.length - 1;
|
renderFormattedText(text, block.id)
|
||||||
|
) : (
|
||||||
|
<span className="text-faint italic select-none"> </span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type InlineSegment =
|
|
||||||
| { kind: 'text'; start: number; end: number; text: string }
|
|
||||||
| { kind: 'token'; start: number; end: number; raw: string };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`w-full space-y-0.5 ${extraTextClass}`}>
|
<textarea
|
||||||
{lines.map((line) => {
|
ref={(el) => {
|
||||||
const isLineActive = isEditing && line.lineIdx === activeLineIdx;
|
blockInputRefs.current[block.id] = el;
|
||||||
const lineTokens = findRichTokens(line.lineText).map((t) => ({
|
if (el) {
|
||||||
start: line.lineStart + t.start,
|
autoResizeTextarea(el);
|
||||||
end: line.lineStart + t.end,
|
|
||||||
raw: t.raw,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Non-active line (or block not currently focused)
|
|
||||||
if (!isLineActive) {
|
|
||||||
const segments: InlineSegment[] = [];
|
|
||||||
let lastIdx = line.lineStart;
|
|
||||||
for (const t of lineTokens) {
|
|
||||||
if (t.start > lastIdx) {
|
|
||||||
segments.push({
|
|
||||||
kind: 'text',
|
|
||||||
start: lastIdx,
|
|
||||||
end: t.start,
|
|
||||||
text: text.slice(lastIdx, t.start),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
segments.push({ kind: 'token', start: t.start, end: t.end, raw: t.raw });
|
|
||||||
lastIdx = t.end;
|
|
||||||
}
|
|
||||||
if (lastIdx < line.lineEnd) {
|
|
||||||
segments.push({
|
|
||||||
kind: 'text',
|
|
||||||
start: lastIdx,
|
|
||||||
end: line.lineEnd,
|
|
||||||
text: text.slice(lastIdx, line.lineEnd),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`line-${line.lineIdx}`}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
e.preventDefault();
|
|
||||||
focusBlock(block.id, line.lineEnd);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
focusBlock(block.id, line.lineEnd);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex flex-wrap items-center gap-y-0.5 w-full text-sm leading-relaxed break-words min-h-[22px] cursor-text"
|
|
||||||
>
|
|
||||||
{segments.length === 0 ? (
|
|
||||||
<span
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
focusBlock(block.id, line.lineStart);
|
|
||||||
}}
|
|
||||||
className="w-full min-h-[22px] inline-block"
|
|
||||||
>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
segments.map((seg, i) =>
|
|
||||||
seg.kind === 'token' ? (
|
|
||||||
renderSingleRichToken(seg.raw, block.id, `tok-${seg.start}-${i}`)
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
key={`txt-${seg.start}-${i}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const sel = window.getSelection();
|
|
||||||
const localOffset =
|
|
||||||
sel && sel.anchorNode && e.currentTarget.contains(sel.anchorNode)
|
|
||||||
? sel.anchorOffset
|
|
||||||
: seg.text.length;
|
|
||||||
focusBlock(block.id, seg.start + localOffset);
|
|
||||||
}}
|
|
||||||
className="whitespace-pre-wrap"
|
|
||||||
>
|
|
||||||
{seg.text}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
// Active line being edited
|
rows={1}
|
||||||
const renderedTokens = lineTokens.filter((t) => !(cursor >= t.start && cursor <= t.end));
|
readOnly={isReadOnly}
|
||||||
const segments: InlineSegment[] = [];
|
value={block.content || ''}
|
||||||
let lastIdx = line.lineStart;
|
placeholder="Notiz eingeben… (/ für Befehle)"
|
||||||
for (const t of renderedTokens) {
|
onFocus={() => {
|
||||||
segments.push({
|
handleBlockFocus();
|
||||||
kind: 'text',
|
}}
|
||||||
start: lastIdx,
|
onBlur={() => {
|
||||||
end: t.start,
|
setActiveEditingBlockId((prev) => (prev === block.id ? null : prev));
|
||||||
text: text.slice(lastIdx, t.start),
|
handleBlockBlur();
|
||||||
});
|
}}
|
||||||
segments.push({
|
onChange={(e) => {
|
||||||
kind: 'token',
|
autoResizeTextarea(e.target);
|
||||||
start: t.start,
|
handleContentChange(block, e.target.value);
|
||||||
end: t.end,
|
}}
|
||||||
raw: t.raw,
|
onKeyDown={(e) => handleKeyDown(e, block, originalIndex)}
|
||||||
});
|
className={`w-full bg-transparent text-sm leading-relaxed focus:outline-none resize-none overflow-hidden break-words whitespace-pre-wrap placeholder:text-faint p-0 m-0 border-0 ${extraTextClass}`}
|
||||||
lastIdx = t.end;
|
/>
|
||||||
}
|
|
||||||
segments.push({
|
|
||||||
kind: 'text',
|
|
||||||
start: lastIdx,
|
|
||||||
end: line.lineEnd,
|
|
||||||
text: text.slice(lastIdx, line.lineEnd),
|
|
||||||
});
|
|
||||||
|
|
||||||
let activeSegIdx = segments.findIndex(
|
|
||||||
(s) => s.kind === 'text' && cursor >= s.start && cursor <= s.end
|
|
||||||
);
|
|
||||||
if (activeSegIdx === -1) {
|
|
||||||
activeSegIdx = segments.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`line-${line.lineIdx}`}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
e.preventDefault();
|
|
||||||
focusBlock(block.id, line.lineEnd);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex flex-wrap items-center gap-y-0.5 w-full min-h-[22px] cursor-text"
|
|
||||||
>
|
|
||||||
{segments.map((seg, idx) => {
|
|
||||||
if (seg.kind === 'token') {
|
|
||||||
return renderSingleRichToken(seg.raw, block.id, `tok-${seg.start}-${idx}`);
|
|
||||||
}
|
|
||||||
if (idx === activeSegIdx) {
|
|
||||||
const isLastSeg = idx === segments.length - 1;
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
key="live-input"
|
|
||||||
ref={(el) => (blockInputRefs.current[block.id] = el)}
|
|
||||||
data-seg-start={seg.start}
|
|
||||||
type="text"
|
|
||||||
readOnly={isReadOnly}
|
|
||||||
value={seg.text}
|
|
||||||
style={
|
|
||||||
!isLastSeg
|
|
||||||
? { width: `${Math.max(1, seg.text.length + 0.5)}ch` }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onFocus={(e) => {
|
|
||||||
const localPos =
|
|
||||||
typeof e.currentTarget.selectionStart === 'number'
|
|
||||||
? e.currentTarget.selectionStart
|
|
||||||
: seg.text.length;
|
|
||||||
setBlockCursor(block.id, seg.start + localPos);
|
|
||||||
handleBlockFocus();
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (
|
|
||||||
e.currentTarget.selectionStart === e.currentTarget.selectionEnd &&
|
|
||||||
typeof e.currentTarget.selectionStart === 'number'
|
|
||||||
) {
|
|
||||||
setBlockCursor(block.id, seg.start + e.currentTarget.selectionStart);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
setActiveEditingBlockId((prev) => (prev === block.id ? null : prev));
|
|
||||||
handleBlockBlur();
|
|
||||||
}}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newSegVal = e.target.value;
|
|
||||||
const localCursor =
|
|
||||||
typeof e.target.selectionStart === 'number'
|
|
||||||
? e.target.selectionStart
|
|
||||||
: newSegVal.length;
|
|
||||||
const newFull = text.slice(0, seg.start) + newSegVal + text.slice(seg.end);
|
|
||||||
const newGlobalCursor = seg.start + localCursor;
|
|
||||||
setBlockCursor(block.id, newGlobalCursor);
|
|
||||||
handleContentChange(block, newFull);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => handleKeyDown(e, block, originalIndex, seg.start)}
|
|
||||||
className={`${
|
|
||||||
isLastSeg ? 'flex-1 min-w-[60px]' : 'flex-shrink-0'
|
|
||||||
} bg-transparent text-sm leading-relaxed focus:outline-none placeholder:text-faint ${extraTextClass}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!seg.text) return null;
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={`txt-${seg.start}-${idx}`}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
focusBlock(block.id, seg.end);
|
|
||||||
}}
|
|
||||||
className="whitespace-pre-wrap text-sm leading-relaxed cursor-text"
|
|
||||||
>
|
|
||||||
{seg.text}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -4512,8 +4317,11 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
rows={1}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) autoResizeTextarea(el);
|
||||||
|
}}
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
value={selectedDoc.title}
|
value={selectedDoc.title}
|
||||||
onFocus={handleBlockFocus}
|
onFocus={handleBlockFocus}
|
||||||
@@ -4521,6 +4329,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (!isReadOnly) {
|
if (!isReadOnly) {
|
||||||
ensureLock();
|
ensureLock();
|
||||||
|
autoResizeTextarea(e.target);
|
||||||
persistDocument({ ...selectedDoc, title: e.target.value });
|
persistDocument({ ...selectedDoc, title: e.target.value });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -4536,7 +4345,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="Dokument-Titel…"
|
placeholder="Dokument-Titel…"
|
||||||
className={`w-full bg-transparent text-xl sm:text-2xl font-bold text-main focus:outline-none placeholder:text-faint ${
|
className={`w-full bg-transparent text-xl sm:text-2xl font-bold text-main focus:outline-none resize-none overflow-hidden break-words whitespace-pre-wrap placeholder:text-faint p-0 m-0 border-0 ${
|
||||||
isReadOnly ? 'cursor-default' : ''
|
isReadOnly ? 'cursor-default' : ''
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@@ -4547,8 +4356,11 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
{zoomedBlock && (
|
{zoomedBlock && (
|
||||||
<div className="pb-2 border-b border-border flex items-center gap-2.5">
|
<div className="pb-2 border-b border-border flex items-center gap-2.5">
|
||||||
<span className="w-2.5 h-2.5 rounded-pill bg-accent flex-shrink-0" />
|
<span className="w-2.5 h-2.5 rounded-pill bg-accent flex-shrink-0" />
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
rows={1}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) autoResizeTextarea(el);
|
||||||
|
}}
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
value={zoomedBlock.content}
|
value={zoomedBlock.content}
|
||||||
onFocus={handleBlockFocus}
|
onFocus={handleBlockFocus}
|
||||||
@@ -4556,6 +4368,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (!isReadOnly) {
|
if (!isReadOnly) {
|
||||||
ensureLock();
|
ensureLock();
|
||||||
|
autoResizeTextarea(e.target);
|
||||||
updateBlock(zoomedBlock.id, { content: e.target.value });
|
updateBlock(zoomedBlock.id, { content: e.target.value });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -4576,7 +4389,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="Aufzählungspunkt…"
|
placeholder="Aufzählungspunkt…"
|
||||||
className={`w-full bg-transparent text-xl font-bold text-main focus:outline-none placeholder:text-faint ${
|
className={`w-full bg-transparent text-xl font-bold text-main focus:outline-none resize-none overflow-hidden break-words whitespace-pre-wrap placeholder:text-faint p-0 m-0 border-0 ${
|
||||||
isReadOnly ? 'cursor-default' : ''
|
isReadOnly ? 'cursor-default' : ''
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@@ -4820,16 +4633,22 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
|
|
||||||
{/* TYPE: H1 */}
|
{/* TYPE: H1 */}
|
||||||
{block.type === 'h1' && (
|
{block.type === 'h1' && (
|
||||||
<input
|
<textarea
|
||||||
ref={(el) => (blockInputRefs.current[block.id] = el)}
|
ref={(el) => {
|
||||||
type="text"
|
blockInputRefs.current[block.id] = el;
|
||||||
|
if (el) autoResizeTextarea(el);
|
||||||
|
}}
|
||||||
|
rows={1}
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
value={block.content}
|
value={block.content}
|
||||||
onFocus={handleBlockFocus}
|
onFocus={handleBlockFocus}
|
||||||
onBlur={handleBlockBlur}
|
onBlur={handleBlockBlur}
|
||||||
onChange={(e) => handleContentChange(block, e.target.value)}
|
onChange={(e) => {
|
||||||
|
autoResizeTextarea(e.target);
|
||||||
|
handleContentChange(block, e.target.value);
|
||||||
|
}}
|
||||||
onKeyDown={(e) => handleKeyDown(e, block, originalIndex)}
|
onKeyDown={(e) => handleKeyDown(e, block, originalIndex)}
|
||||||
className={`w-full bg-transparent text-lg font-bold text-main focus:outline-none ${
|
className={`w-full bg-transparent text-lg font-bold text-main focus:outline-none resize-none overflow-hidden break-words whitespace-pre-wrap p-0 m-0 border-0 ${
|
||||||
isReadOnly ? 'cursor-default' : ''
|
isReadOnly ? 'cursor-default' : ''
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@@ -4837,16 +4656,22 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
|
|
||||||
{/* TYPE: H2 */}
|
{/* TYPE: H2 */}
|
||||||
{block.type === 'h2' && (
|
{block.type === 'h2' && (
|
||||||
<input
|
<textarea
|
||||||
ref={(el) => (blockInputRefs.current[block.id] = el)}
|
ref={(el) => {
|
||||||
type="text"
|
blockInputRefs.current[block.id] = el;
|
||||||
|
if (el) autoResizeTextarea(el);
|
||||||
|
}}
|
||||||
|
rows={1}
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
value={block.content}
|
value={block.content}
|
||||||
onFocus={handleBlockFocus}
|
onFocus={handleBlockFocus}
|
||||||
onBlur={handleBlockBlur}
|
onBlur={handleBlockBlur}
|
||||||
onChange={(e) => handleContentChange(block, e.target.value)}
|
onChange={(e) => {
|
||||||
|
autoResizeTextarea(e.target);
|
||||||
|
handleContentChange(block, e.target.value);
|
||||||
|
}}
|
||||||
onKeyDown={(e) => handleKeyDown(e, block, originalIndex)}
|
onKeyDown={(e) => handleKeyDown(e, block, originalIndex)}
|
||||||
className={`w-full bg-transparent text-base font-semibold text-accent focus:outline-none ${
|
className={`w-full bg-transparent text-base font-semibold text-accent focus:outline-none resize-none overflow-hidden break-words whitespace-pre-wrap p-0 m-0 border-0 ${
|
||||||
isReadOnly ? 'cursor-default' : ''
|
isReadOnly ? 'cursor-default' : ''
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@@ -4854,16 +4679,22 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
|
|
||||||
{/* TYPE: H3 */}
|
{/* TYPE: H3 */}
|
||||||
{block.type === 'h3' && (
|
{block.type === 'h3' && (
|
||||||
<input
|
<textarea
|
||||||
ref={(el) => (blockInputRefs.current[block.id] = el)}
|
ref={(el) => {
|
||||||
type="text"
|
blockInputRefs.current[block.id] = el;
|
||||||
|
if (el) autoResizeTextarea(el);
|
||||||
|
}}
|
||||||
|
rows={1}
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
value={block.content}
|
value={block.content}
|
||||||
onFocus={handleBlockFocus}
|
onFocus={handleBlockFocus}
|
||||||
onBlur={handleBlockBlur}
|
onBlur={handleBlockBlur}
|
||||||
onChange={(e) => handleContentChange(block, e.target.value)}
|
onChange={(e) => {
|
||||||
|
autoResizeTextarea(e.target);
|
||||||
|
handleContentChange(block, e.target.value);
|
||||||
|
}}
|
||||||
onKeyDown={(e) => handleKeyDown(e, block, originalIndex)}
|
onKeyDown={(e) => handleKeyDown(e, block, originalIndex)}
|
||||||
className={`w-full bg-transparent text-sm font-semibold text-main focus:outline-none ${
|
className={`w-full bg-transparent text-sm font-semibold text-main focus:outline-none resize-none overflow-hidden break-words whitespace-pre-wrap p-0 m-0 border-0 ${
|
||||||
isReadOnly ? 'cursor-default' : ''
|
isReadOnly ? 'cursor-default' : ''
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in new issue
Block a user