From 2d73c458709feb9e513779037ee14de680ed1832 Mon Sep 17 00:00:00 2001 From: sodenn Date: Sun, 9 Jun 2024 22:41:30 +0200 Subject: [PATCH 1/5] fix: end-of-line cursor issue in Safari --- packages/lexical/src/LexicalMutations.ts | 1 - packages/lexical/src/LexicalReconciler.ts | 56 +++++++++++++++++------ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/packages/lexical/src/LexicalMutations.ts b/packages/lexical/src/LexicalMutations.ts index 15e4e510d39..d563a13fa85 100644 --- a/packages/lexical/src/LexicalMutations.ts +++ b/packages/lexical/src/LexicalMutations.ts @@ -57,7 +57,6 @@ function isManagedLineBreak( editor: LexicalEditor, ): boolean { return ( - // @ts-expect-error: internal field target.__lexicalLineBreak === dom || // @ts-ignore We intentionally add this to the Node. dom[`__lexicalKey_${editor._key}`] !== undefined diff --git a/packages/lexical/src/LexicalReconciler.ts b/packages/lexical/src/LexicalReconciler.ts index 87c24437342..a2b0842ba0e 100644 --- a/packages/lexical/src/LexicalReconciler.ts +++ b/packages/lexical/src/LexicalReconciler.ts @@ -16,6 +16,7 @@ import type { import type {NodeKey, NodeMap} from './LexicalNode'; import type {ElementNode} from './nodes/LexicalElementNode'; +import {IS_SAFARI} from 'shared/environment'; import invariant from 'shared/invariant'; import normalizeClassNames from 'shared/normalizeClassNames'; @@ -48,6 +49,13 @@ import { type IntentionallyMarkedAsDirtyElement = boolean; +declare global { + interface Node { + __lexicalLineBreak?: HTMLBRElement | null; + __lexicalLineBreakImg?: HTMLImageElement | null; + } +} + let subTreeTextContent = ''; let subTreeDirectionedTextContent = ''; let subTreeTextFormat: number | null = null; @@ -234,8 +242,8 @@ function $createNode( if (insertDOM != null) { parentDOM.insertBefore(dom, insertDOM); } else { - // @ts-expect-error: internal field - const possibleLineBreak = parentDOM.__lexicalLineBreak; + const possibleLineBreak = + parentDOM.__lexicalLineBreakImg ?? parentDOM.__lexicalLineBreak; if (possibleLineBreak != null) { parentDOM.insertBefore(dom, possibleLineBreak); @@ -330,24 +338,47 @@ function reconcileElementTerminatingLineBreak( if (prevLineBreak) { if (!nextLineBreak) { - // @ts-expect-error: internal field - const element = dom.__lexicalLineBreak; - - if (element != null) { - dom.removeChild(element); - } - - // @ts-expect-error: internal field - dom.__lexicalLineBreak = null; + const removeElement = ( + fieldName: '__lexicalLineBreak' | '__lexicalLineBreakImg', + ) => { + const element = dom[fieldName]; + if (element != null) { + dom.removeChild(element); + dom[fieldName] = null; + } + }; + removeElement('__lexicalLineBreak'); + removeElement('__lexicalLineBreakImg'); } } else if (nextLineBreak) { const element = document.createElement('br'); - // @ts-expect-error: internal field + // Workaround for a bug in Safari where the cursor cannot be placed at the + // end of a line. + if (IS_SAFARI) { + insertCursorFixElement(dom, element); + } dom.__lexicalLineBreak = element; dom.appendChild(element); } } +function insertCursorFixElement( + dom: HTMLElement, + lineBreakElement: HTMLBRElement, +): void { + const element = document.createElement('img'); + element.alt = ''; + + // prevent conflict with other css rules + const styles = element.style; + styles.setProperty('display', 'inline', 'important'); + styles.setProperty('border', 'none', 'important'); + styles.setProperty('margin', '0', 'important'); + + dom.appendChild(element); + dom.__lexicalLineBreakImg = element; +} + function reconcileParagraphFormat(element: ElementNode): void { if ( $isParagraphNode(element) && @@ -505,7 +536,6 @@ function $reconcileChildren( } } else if (nextChildrenSize === 0) { if (prevChildrenSize !== 0) { - // @ts-expect-error: internal field const lexicalLineBreak = dom.__lexicalLineBreak; const canUseFastPath = lexicalLineBreak == null; destroyChildren( From fecb88cfae8b74bb4b46462079f04418db80db03 Mon Sep 17 00:00:00 2001 From: sodenn Date: Mon, 10 Jun 2024 19:16:48 +0200 Subject: [PATCH 2/5] replace nullish coalescing operator for ECMAScript compatibility reasons --- packages/lexical/src/LexicalReconciler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical/src/LexicalReconciler.ts b/packages/lexical/src/LexicalReconciler.ts index a2b0842ba0e..975c7da023c 100644 --- a/packages/lexical/src/LexicalReconciler.ts +++ b/packages/lexical/src/LexicalReconciler.ts @@ -243,7 +243,7 @@ function $createNode( parentDOM.insertBefore(dom, insertDOM); } else { const possibleLineBreak = - parentDOM.__lexicalLineBreakImg ?? parentDOM.__lexicalLineBreak; + parentDOM.__lexicalLineBreakImg || parentDOM.__lexicalLineBreak; if (possibleLineBreak != null) { parentDOM.insertBefore(dom, possibleLineBreak); From ebde618d4763c1e44582f2c9015219b761d82ecf Mon Sep 17 00:00:00 2001 From: sodenn Date: Tue, 11 Jun 2024 22:28:31 +0200 Subject: [PATCH 3/5] remove global Node interface extension as fields are for internal use only --- packages/lexical/src/LexicalMutations.ts | 1 + packages/lexical/src/LexicalReconciler.ts | 17 +++++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/lexical/src/LexicalMutations.ts b/packages/lexical/src/LexicalMutations.ts index d563a13fa85..15e4e510d39 100644 --- a/packages/lexical/src/LexicalMutations.ts +++ b/packages/lexical/src/LexicalMutations.ts @@ -57,6 +57,7 @@ function isManagedLineBreak( editor: LexicalEditor, ): boolean { return ( + // @ts-expect-error: internal field target.__lexicalLineBreak === dom || // @ts-ignore We intentionally add this to the Node. dom[`__lexicalKey_${editor._key}`] !== undefined diff --git a/packages/lexical/src/LexicalReconciler.ts b/packages/lexical/src/LexicalReconciler.ts index 975c7da023c..97a10f065d4 100644 --- a/packages/lexical/src/LexicalReconciler.ts +++ b/packages/lexical/src/LexicalReconciler.ts @@ -49,13 +49,6 @@ import { type IntentionallyMarkedAsDirtyElement = boolean; -declare global { - interface Node { - __lexicalLineBreak?: HTMLBRElement | null; - __lexicalLineBreakImg?: HTMLImageElement | null; - } -} - let subTreeTextContent = ''; let subTreeDirectionedTextContent = ''; let subTreeTextFormat: number | null = null; @@ -243,6 +236,7 @@ function $createNode( parentDOM.insertBefore(dom, insertDOM); } else { const possibleLineBreak = + // @ts-expect-error: internal field parentDOM.__lexicalLineBreakImg || parentDOM.__lexicalLineBreak; if (possibleLineBreak != null) { @@ -338,12 +332,12 @@ function reconcileElementTerminatingLineBreak( if (prevLineBreak) { if (!nextLineBreak) { - const removeElement = ( - fieldName: '__lexicalLineBreak' | '__lexicalLineBreakImg', - ) => { + const removeElement = (fieldName: string) => { + // @ts-expect-error: internal field const element = dom[fieldName]; if (element != null) { dom.removeChild(element); + // @ts-expect-error: internal field dom[fieldName] = null; } }; @@ -357,6 +351,7 @@ function reconcileElementTerminatingLineBreak( if (IS_SAFARI) { insertCursorFixElement(dom, element); } + // @ts-expect-error: internal field dom.__lexicalLineBreak = element; dom.appendChild(element); } @@ -376,6 +371,7 @@ function insertCursorFixElement( styles.setProperty('margin', '0', 'important'); dom.appendChild(element); + // @ts-expect-error: internal field dom.__lexicalLineBreakImg = element; } @@ -536,6 +532,7 @@ function $reconcileChildren( } } else if (nextChildrenSize === 0) { if (prevChildrenSize !== 0) { + // @ts-expect-error: internal field const lexicalLineBreak = dom.__lexicalLineBreak; const canUseFastPath = lexicalLineBreak == null; destroyChildren( From 5705d62aef268f5879275779158f9f2373836577 Mon Sep 17 00:00:00 2001 From: sodenn Date: Mon, 17 Jun 2024 17:20:14 +0200 Subject: [PATCH 4/5] consider mobile Safari as well --- packages/lexical/src/LexicalReconciler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lexical/src/LexicalReconciler.ts b/packages/lexical/src/LexicalReconciler.ts index 97a10f065d4..1f4b5a35308 100644 --- a/packages/lexical/src/LexicalReconciler.ts +++ b/packages/lexical/src/LexicalReconciler.ts @@ -16,7 +16,7 @@ import type { import type {NodeKey, NodeMap} from './LexicalNode'; import type {ElementNode} from './nodes/LexicalElementNode'; -import {IS_SAFARI} from 'shared/environment'; +import {IS_IOS, IS_SAFARI} from 'shared/environment'; import invariant from 'shared/invariant'; import normalizeClassNames from 'shared/normalizeClassNames'; @@ -348,7 +348,7 @@ function reconcileElementTerminatingLineBreak( const element = document.createElement('br'); // Workaround for a bug in Safari where the cursor cannot be placed at the // end of a line. - if (IS_SAFARI) { + if (IS_SAFARI || IS_IOS) { insertCursorFixElement(dom, element); } // @ts-expect-error: internal field From ec0034dae37b5460960b6914855610d7c91745a3 Mon Sep 17 00:00:00 2001 From: sodenn Date: Tue, 25 Jun 2024 20:19:21 +0200 Subject: [PATCH 5/5] remove unnecessary `@ts-expect-error` suppressions --- packages/lexical/src/LexicalReconciler.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/lexical/src/LexicalReconciler.ts b/packages/lexical/src/LexicalReconciler.ts index 1f4b5a35308..870bce493ad 100644 --- a/packages/lexical/src/LexicalReconciler.ts +++ b/packages/lexical/src/LexicalReconciler.ts @@ -333,11 +333,9 @@ function reconcileElementTerminatingLineBreak( if (prevLineBreak) { if (!nextLineBreak) { const removeElement = (fieldName: string) => { - // @ts-expect-error: internal field const element = dom[fieldName]; if (element != null) { dom.removeChild(element); - // @ts-expect-error: internal field dom[fieldName] = null; } };