Desktop: Fixes #12059: Fix Rich Text Editor deletes paragraphs when pressing enter after a resized image (#12090)

This commit is contained in:
Henry Heino
2025-04-14 18:52:10 +01:00
committed by GitHub
parent 9871717de4
commit 81f5a8463e
3 changed files with 65 additions and 15 deletions
@@ -57,7 +57,7 @@ const logger = Logger.create('TinyMCE');
//
// The problem is that the list plugin was, unknown to me, relying on this <br/>
// being present. Without it, trying to add a bullet point or checkbox on an
// empty document, does nothing. The exact reason for this is unclear
// empty document, adds an empty paragraph. The exact reason for this is unclear
// so as a workaround we manually add this <br> for empty documents,
// which fixes the issue.
//
@@ -70,8 +70,8 @@ const logger = Logger.create('TinyMCE');
//
// Perhaps upgrading the list plugin (which is a fork of TinyMCE own list plugin)
// would help?
function awfulInitHack(html: string): string {
return html === '<div id="rendered-md"></div>' ? '<div id="rendered-md"><p></p></div>' : html;
function preprocessHtml(html: string): string {
return html === '' ? '<p></p>' : html;
}
@@ -1045,6 +1045,11 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// This prevents HTML-style resource URLs (e.g. <a href="file://path/to/resource/.../"></a>)
// from being discarded.
allowedFilePrefixes: [props.resourceDirectory],
// Remove the wrapping <div id="rendered-md">...</div>, which can cause
// TinyMCE to crash in some cases.
// See https://github.com/tinymce/tinymce/issues/10276
bodyOnly: true,
}),
);
if (cancelled) return;
@@ -1056,7 +1061,11 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// when the note content is updated externally.
const offsetBookmarkId = 2;
const bookmark = editor.selection.getBookmark(offsetBookmarkId);
editor.setContent(awfulInitHack(result.html));
const htmlAndCss = [
`<style>${result.cssStrings?.join('\n')}</style>`,
preprocessHtml(result.html),
].join('\n');
editor.setContent(htmlAndCss);
lastNoteIdRef.current = props.noteId;
if (lastOnChangeEventInfo.current.contentKey !== props.contentKey) {
@@ -1089,6 +1098,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const allAssetsOptions: NoteStyleOptions = {
contentMaxWidthTarget: '.mce-content-body',
contentWrapperSelector: '.mce-content-body',
scrollbarSize: props.scrollbarSize,
themeId: props.contentMarkupLanguage === MarkupLanguage.Html ? 1 : null,
whiteBackgroundNoteRendering: props.whiteBackgroundNoteRendering,
@@ -85,6 +85,44 @@ test.describe('richTextEditor', () => {
expect(await openPathResult).toContain(basename(pathToAttach));
});
test('should not remove text when pressing [enter] at the end of a line with an image', async ({ mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.createNewNote('Testing pressing enter!');
const editor = mainScreen.noteEditor;
// Set the initial content
await editor.codeMirrorEditor.click();
await mainWindow.keyboard.type([
'<img',
' src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAAEklEQVQIW2P8z8AARBDAiJMDAIzoBf635fcVAAAAAElFTkSuQmCC"',
' width="200"',
' height="200"',
' alt="test image"',
'/>',
].join(' '));
await mainWindow.keyboard.press('Enter');
await mainWindow.keyboard.press('Enter');
await mainWindow.keyboard.type('Test secondary paragraph.');
// Switch to the RTE
await editor.toggleEditorsButton.click();
await editor.richTextEditor.waitFor();
const richTextEditorFrame = editor.getRichTextFrameLocator();
const testParagraph = richTextEditorFrame.getByText('Test secondary paragraph.');
await expect(testParagraph).toBeAttached();
// Move the cursor just after the image, then press enter.
const testImage = richTextEditorFrame.getByRole('img', { name: 'test image' });
await testImage.click();
await mainWindow.keyboard.press('ArrowRight');
await mainWindow.keyboard.press('Enter');
// Should not have removed the image or the test paragraph.
await expect(testImage).toBeAttached();
await expect(testParagraph).toBeAttached();
});
test('pressing Tab should indent', async ({ mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.createNewNote('Testing tabs!');
+13 -11
View File
@@ -11,6 +11,7 @@ function formatCssSize(v: any): string {
export interface Options {
contentMaxWidth?: number;
contentMaxWidthTarget?: string;
contentWrapperSelector?: string;
scrollbarSize?: number;
themeId?: number;
whiteBackgroundNoteRendering?: boolean;
@@ -84,7 +85,8 @@ export default function(theme: any, options: Options = null) {
const fontFamily = '\'Avenir Next\', \'Avenir\', \'Arial\', sans-serif';
const maxWidthTarget = options.contentMaxWidthTarget ? options.contentMaxWidthTarget : '#rendered-md';
const contentWrapperTarget = options.contentWrapperSelector ?? '#rendered-md';
const maxWidthTarget = options.contentMaxWidthTarget ? options.contentMaxWidthTarget : contentWrapperTarget;
const maxWidthCss = options.contentMaxWidth ? `
${maxWidthTarget} {
max-width: ${options.contentMaxWidth}px;
@@ -150,16 +152,16 @@ export default function(theme: any, options: Options = null) {
/* Remove top padding and margin from first child so that top of rendered text is aligned to top of text editor text */
#rendered-md > h1:first-child,
#rendered-md > h2:first-child,
#rendered-md > h3:first-child,
#rendered-md > h4:first-child,
#rendered-md > ul:first-child,
#rendered-md > ol:first-child,
#rendered-md > table:first-child,
#rendered-md > blockquote:first-child,
#rendered-md > img:first-child,
#rendered-md > p:first-child {
${contentWrapperTarget} > h1:first-child,
${contentWrapperTarget} > h2:first-child,
${contentWrapperTarget} > h3:first-child,
${contentWrapperTarget} > h4:first-child,
${contentWrapperTarget} > ul:first-child,
${contentWrapperTarget} > ol:first-child,
${contentWrapperTarget} > table:first-child,
${contentWrapperTarget} > blockquote:first-child,
${contentWrapperTarget} > img:first-child,
${contentWrapperTarget} > p:first-child {
margin-top: 0;
padding-top: 0;
}