diff --git a/apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/text.tsx b/apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/text.tsx index 963ea6cecd..30066193b8 100644 --- a/apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/text.tsx +++ b/apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/text.tsx @@ -20,6 +20,44 @@ interface TextEditorProps { isDisabled?: boolean; } +// Helper functions to convert between formats +const contentHelpers = { + // Convert content with newlines to ProseMirror nodes + createNodesFromContent: (content: string) => { + if (!content) return []; + + const lines = content.split('\n'); + const nodes = []; + + for (let i = 0; i < lines.length; i++) { + if (lines[i] || i === 0) { + nodes.push(schema.text(lines[i] || '')); + } + if (i < lines.length - 1) { + const hardBreakNode = schema.nodes.hard_break; + if (hardBreakNode) { + nodes.push(hardBreakNode.create()); + } + } + } + + return nodes; + }, + + // Convert ProseMirror document to text with newlines + extractContentWithNewlines: (view: EditorView) => { + let content = ''; + view.state.doc.descendants((node) => { + if (node.type.name === 'text' && node.text) { + content += node.text || ''; + } else if (node.type.name === 'hard_break') { + content += '\n'; + } + }); + return content; + } +}; + export const TextEditor: React.FC = ({ rect, content, @@ -50,7 +88,8 @@ export const TextEditor: React.FC = ({ const newState = view.state.apply(transaction); view.updateState(newState); if (onChange && transaction.docChanged) { - onChange(view.state.doc.textContent); + const textContent = contentHelpers.extractContentWithNewlines(view); + onChange(textContent); } }, attributes: { @@ -60,8 +99,9 @@ export const TextEditor: React.FC = ({ editorViewRef.current = view; - // Set initial content - const paragraph = schema.node('paragraph', null, content ? [schema.text(content)] : []); + // Set initial content with proper line break handling + const nodes = contentHelpers.createNodesFromContent(content); + const paragraph = schema.node('paragraph', null, nodes); const newDoc = schema.node('doc', null, [paragraph]); const tr = view.state.tr.replaceWith(0, view.state.doc.content.size, newDoc.content); view.dispatch(tr); diff --git a/apps/web/client/src/components/store/editor/overlay/prosemirror/index.ts b/apps/web/client/src/components/store/editor/overlay/prosemirror/index.ts index 2b13efc3c5..f1457d7087 100644 --- a/apps/web/client/src/components/store/editor/overlay/prosemirror/index.ts +++ b/apps/web/client/src/components/store/editor/overlay/prosemirror/index.ts @@ -3,7 +3,7 @@ import { baseKeymap } from 'prosemirror-commands'; import { history, redo, undo } from 'prosemirror-history'; import { keymap } from 'prosemirror-keymap'; import { Schema } from 'prosemirror-model'; -import { Plugin } from 'prosemirror-state'; +import { Plugin, EditorState } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { adaptValueToCanvas } from '../utils'; @@ -11,10 +11,16 @@ export const schema = new Schema({ nodes: { doc: { content: 'paragraph+' }, paragraph: { - content: 'text*', + content: '(text | hard_break)*', toDOM: () => ['p', { style: 'margin: 0; padding: 0;' }, 0], }, text: { inline: true }, + hard_break: { + inline: true, + group: 'inline', + selectable: false, + toDOM: () => ['br'], + }, }, marks: { style: { @@ -34,15 +40,13 @@ export const schema = new Schema({ export function applyStylesToEditor(editorView: EditorView, styles: Record) { const { state, dispatch } = editorView; - const { tr } = state; const styleMark = state.schema.marks?.style; if (!styleMark) { console.error('No style mark found'); return; } - tr.addMark(0, state.doc.content.size, styleMark.create({ style: styles })); - // Apply container styles + const tr = state.tr.addMark(0, state.doc.content.size, styleMark.create({ style: styles })); const fontSize = adaptValueToCanvas(parseFloat(styles.fontSize ?? '')); const lineHeight = adaptValueToCanvas(parseFloat(styles.lineHeight ?? '')); @@ -63,31 +67,37 @@ export function applyStylesToEditor(editorView: EditorView, styles: Record (state: EditorState, dispatch?: (tr: any) => void) => { + if (dispatch) { + const hardBreakNode = state.schema.nodes.hard_break; + if (hardBreakNode) { + dispatch(state.tr.replaceSelectionWith(hardBreakNode.create())); + } + } + return true; +}; + +const createEnterHandler = (onExit: () => void) => (state: EditorState) => { + onExit(); + return true; +}; + export const createEditorPlugins = (onEscape?: () => void, onEnter?: () => void): Plugin[] => [ history(), keymap({ 'Mod-z': undo, 'Mod-shift-z': redo, Escape: () => { - if (onEscape) { - onEscape(); - return true; - } - return false; - }, - Enter: () => { - if (onEnter) { - onEnter(); - return true; - } - return false; + onEscape?.(); + return !!onEscape; }, + Enter: onEnter ? createEnterHandler(onEnter) : () => false, + 'Shift-Enter': createLineBreakHandler(), }), keymap(baseKeymap), -]; +]; \ No newline at end of file diff --git a/apps/web/client/src/components/store/editor/text/index.ts b/apps/web/client/src/components/store/editor/text/index.ts index a05ae9d740..e99e467c02 100644 --- a/apps/web/client/src/components/store/editor/text/index.ts +++ b/apps/web/client/src/components/store/editor/text/index.ts @@ -160,7 +160,6 @@ export class TextEditingManager { originalContent: this.originalContent ?? '', newContent, }); - const adjustedRect = adaptRectToCanvas(domEl.rect, frameView); this.editorEngine.overlay.state.updateTextEditor(adjustedRect); await this.editorEngine.overlay.refresh(); diff --git a/apps/web/preload/dist/index.js b/apps/web/preload/dist/index.js index 2309b4184e..09e68fcc64 100644 --- a/apps/web/preload/dist/index.js +++ b/apps/web/preload/dist/index.js @@ -12889,16 +12889,19 @@ function startEditingText(domId) { } const childNodes = Array.from(el.childNodes).filter((node) => node.nodeType !== Node.COMMENT_NODE); let targetEl = null; + const hasOnlyTextAndBreaks = childNodes.every((node) => node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === "br"); if (childNodes.length === 0) { targetEl = el; - } else if (childNodes.length === 1 && el.childNodes[0]?.nodeType === Node.TEXT_NODE) { + } else if (childNodes.length === 1 && childNodes[0]?.nodeType === Node.TEXT_NODE) { + targetEl = el; + } else if (hasOnlyTextAndBreaks) { targetEl = el; } if (!targetEl) { console.warn("Start editing text failed. No target element found for selector:", domId); return null; } - const originalContent = el.textContent || ""; + const originalContent = extractTextContent(el); prepareElementForEditing(targetEl); return { originalContent }; } @@ -12920,7 +12923,7 @@ function stopEditingText(domId) { } cleanUpElementAfterEditing(el); publishEditText(getDomElement(el, true)); - return { newContent: el.textContent || "", domEl: getDomElement(el, true) }; + return { newContent: extractTextContent(el), domEl: getDomElement(el, true) }; } function prepareElementForEditing(el) { el.setAttribute("data-onlook-editing-text" /* DATA_ONLOOK_EDITING_TEXT */, "true"); @@ -12933,7 +12936,17 @@ function removeEditingAttributes(el) { el.removeAttribute("data-onlook-editing-text" /* DATA_ONLOOK_EDITING_TEXT */); } function updateTextContent(el, content) { - el.textContent = content; + const htmlContent = content.replace(/\n/g, "
"); + el.innerHTML = htmlContent; +} +function extractTextContent(el) { + let content = el.innerHTML; + content = content.replace(//gi, ` +`); + content = content.replace(/<[^>]*>/g, ""); + const textArea = document.createElement("textarea"); + textArea.innerHTML = content; + return textArea.value; } function isChildTextEditable(oid) { return true; @@ -17359,5 +17372,5 @@ export { penpalParent }; -//# debugId=063B7F3A5134580A64756E2164756E21 +//# debugId=1F8F09CA958308FB64756E2164756E21 //# sourceMappingURL=index.js.map diff --git a/apps/web/preload/script/api/elements/text.ts b/apps/web/preload/script/api/elements/text.ts index bc79e8f9c3..592aa8490f 100644 --- a/apps/web/preload/script/api/elements/text.ts +++ b/apps/web/preload/script/api/elements/text.ts @@ -25,16 +25,27 @@ export function startEditingText(domId: string): EditTextResult | null { ); let targetEl: HTMLElement | null = null; + // Check for element type + const hasOnlyTextAndBreaks = childNodes.every(node => + node.nodeType === Node.TEXT_NODE || + (node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName.toLowerCase() === 'br') + ); + if (childNodes.length === 0) { targetEl = el as HTMLElement; - } else if (childNodes.length === 1 && el.childNodes[0]?.nodeType === Node.TEXT_NODE) { + } else if (childNodes.length === 1 && childNodes[0]?.nodeType === Node.TEXT_NODE) { + targetEl = el as HTMLElement; + } else if (hasOnlyTextAndBreaks) { + // Handle elements with text and
tags targetEl = el as HTMLElement; } + if (!targetEl) { console.warn('Start editing text failed. No target element found for selector:', domId); return null; } - const originalContent = el.textContent || ''; + + const originalContent = extractTextContent(el); prepareElementForEditing(targetEl); return { originalContent }; @@ -59,7 +70,7 @@ export function stopEditingText(domId: string): { newContent: string; domEl: Dom } cleanUpElementAfterEditing(el); publishEditText(getDomElement(el, true)); - return { newContent: el.textContent || '', domEl: getDomElement(el, true) }; + return { newContent: extractTextContent(el), domEl: getDomElement(el, true) }; } function prepareElementForEditing(el: HTMLElement) { @@ -76,7 +87,18 @@ function removeEditingAttributes(el: HTMLElement) { } function updateTextContent(el: HTMLElement, content: string): void { - el.textContent = content; + // Convert newlines to
tags in the DOM + const htmlContent = content.replace(/\n/g, '
'); + el.innerHTML = htmlContent; +} + +function extractTextContent(el: HTMLElement): string { + let content = el.innerHTML; + content = content.replace(//gi, '\n'); + content = content.replace(/<[^>]*>/g, ''); + const textArea = document.createElement('textarea'); + textArea.innerHTML = content; + return textArea.value; } export function isChildTextEditable(oid: string): boolean | null { diff --git a/packages/parser/src/code-edit/text.ts b/packages/parser/src/code-edit/text.ts index 12e048749d..5c374e528f 100644 --- a/packages/parser/src/code-edit/text.ts +++ b/packages/parser/src/code-edit/text.ts @@ -1,10 +1,32 @@ import { type t as T, types as t } from '../packages'; export function updateNodeTextContent(node: T.JSXElement, textContent: string): void { - const textNode = node.children.find((child) => t.isJSXText(child)) as T.JSXText | undefined; - if (textNode) { - textNode.value = textContent; - } else { - node.children.unshift(t.jsxText(textContent)); + // Split the text content by newlines + const parts = textContent.split('\n'); + + // If there's only one part (no newlines), handle as before + if (parts.length === 1) { + const textNode = node.children.find((child) => t.isJSXText(child)) as T.JSXText | undefined; + if (textNode) { + textNode.value = textContent; + } else { + node.children.unshift(t.jsxText(textContent)); + } + return; } + + // Clear existing children + node.children = []; + + // Add each part with a
in between + parts.forEach((part, index) => { + if (part) { + node.children.push(t.jsxText(part)); + } + if (index < parts.length - 1) { + node.children.push( + t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier('br'), [], true), null, [], true), + ); + } + }); }