Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<TextEditorProps> = ({
rect,
content,
Expand Down Expand Up @@ -50,7 +88,8 @@ export const TextEditor: React.FC<TextEditorProps> = ({
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: {
Expand All @@ -60,8 +99,9 @@ export const TextEditor: React.FC<TextEditorProps> = ({

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@ 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';

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: {
Expand All @@ -34,15 +40,13 @@ export const schema = new Schema({

export function applyStylesToEditor(editorView: EditorView, styles: Record<string, string>) {
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 ?? ''));

Expand All @@ -63,31 +67,37 @@ export function applyStylesToEditor(editorView: EditorView, styles: Record<strin
backgroundColor: styles.backgroundColor,
wordBreak: 'break-word',
overflow: 'visible',
height: '100%',
});
editorView.dom.style.height = '100%';
dispatch(tr);
}

// Export common plugins configuration
const createLineBreakHandler = () => (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),
];
];
1 change: 0 additions & 1 deletion apps/web/client/src/components/store/editor/text/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
23 changes: 18 additions & 5 deletions apps/web/preload/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand All @@ -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");
Expand All @@ -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, "<br>");
el.innerHTML = htmlContent;
}
function extractTextContent(el) {
let content = el.innerHTML;
content = content.replace(/<br\s*\/?>/gi, `
`);
content = content.replace(/<[^>]*>/g, "");
const textArea = document.createElement("textarea");
textArea.innerHTML = content;
return textArea.value;
}
function isChildTextEditable(oid) {
return true;
Expand Down Expand Up @@ -17359,5 +17372,5 @@ export {
penpalParent
};

//# debugId=063B7F3A5134580A64756E2164756E21
//# debugId=1F8F09CA958308FB64756E2164756E21
//# sourceMappingURL=index.js.map
30 changes: 26 additions & 4 deletions apps/web/preload/script/api/elements/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <br> 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 };
Expand All @@ -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) {
Expand All @@ -76,7 +87,18 @@ function removeEditingAttributes(el: HTMLElement) {
}

function updateTextContent(el: HTMLElement, content: string): void {
el.textContent = content;
// Convert newlines to <br> tags in the DOM
const htmlContent = content.replace(/\n/g, '<br>');
el.innerHTML = htmlContent;
}

function extractTextContent(el: HTMLElement): string {
let content = el.innerHTML;
content = content.replace(/<br\s*\/?>/gi, '\n');
content = content.replace(/<[^>]*>/g, '');
const textArea = document.createElement('textarea');
textArea.innerHTML = content;
return textArea.value;
}

export function isChildTextEditable(oid: string): boolean | null {
Expand Down
32 changes: 27 additions & 5 deletions packages/parser/src/code-edit/text.ts
Original file line number Diff line number Diff line change
@@ -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 <br/> 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),
);
}
});
}