Skip to content

Commit 6042c05

Browse files
Fix for pasted content position in the rich text editor (#4720)
1 parent aa7ace4 commit 6042c05

File tree

4 files changed

+87
-43
lines changed

4 files changed

+87
-43
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "patch",
3+
"area": "fix",
4+
"workstream": "RTE",
5+
"comment": "Fix an issue where rich text editor wasn't scrolled content correctly when a new content was pasted",
6+
"packageName": "@azure/communication-react",
7+
"email": "[email protected]",
8+
"dependentChangeType": "patch"
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "patch",
3+
"area": "fix",
4+
"workstream": "RTE",
5+
"comment": "Fix an issue where rich text editor wasn't scrolled content correctly when a new content was pasted",
6+
"packageName": "@azure/communication-react",
7+
"email": "[email protected]",
8+
"dependentChangeType": "patch"
9+
}

common/config/rush/pnpm-lock.yaml

+33-20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-components/src/components/RichTextEditor/Plugins/CopyPastePlugin.tsx

+36-23
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default class CopyPastePlugin implements EditorPlugin {
2727

2828
if (this.editor !== null && !this.editor.isDisposed()) {
2929
// scroll the editor to the correct position after pasting content
30-
scrollToBottomAfterContentPaste(event, this.editor);
30+
scrollToBottomAfterContentPaste(event);
3131
}
3232
}
3333
}
@@ -71,32 +71,45 @@ export const removeImageElement = (event: BeforePasteEvent): void => {
7171
};
7272

7373
/**
74-
* Scrolls the editor's scroll container to the bottom after content is pasted.
74+
* Update the scroll position of the editor after pasting content to ensure the content is visible.
7575
* @param event - The plugin event.
76-
* @param editor - The editor instance.
7776
*/
78-
export const scrollToBottomAfterContentPaste = (event: PluginEvent, editor: IEditor): void => {
77+
export const scrollToBottomAfterContentPaste = (event: PluginEvent): void => {
7978
if (event.eventType === PluginEventType.ContentChanged && event.source === ContentChangedEventSource.Paste) {
80-
const scrollContainer = editor.getScrollContainer();
81-
// get current selection for the editor
79+
// Get the current selection in the document
8280
const selection = document.getSelection();
83-
// if selection exists
84-
if (selection && selection.rangeCount > 0) {
85-
// the range for the selection
86-
const range = selection.getRangeAt(0);
87-
// the selection position relative to the viewport
88-
const cursorRect = range.getBoundingClientRect();
89-
// the scrollContainer position relative to the viewport
90-
const scrollContainerRect = scrollContainer.getBoundingClientRect();
91-
// 1. scrollContainer.scrollTop represents the number of pixels that the content of scrollContainer is scrolled upward.
92-
// 2. subtract the top position of the scrollContainer element (scrollContainerRect.top) to
93-
// translate the scroll position from being relative to the document to being relative to the viewport.
94-
// 3. add the top position of the cursor (containerRect.top) to moves the scroll position to the cursor's position.
95-
const updatedScrollTop = scrollContainer.scrollTop - scrollContainerRect.top + cursorRect.top;
96-
scrollContainer.scrollTo({
97-
top: updatedScrollTop,
98-
behavior: 'smooth'
99-
});
81+
82+
// Check if a selection exists and it has at least one range
83+
if (!selection || selection.rangeCount <= 0) {
84+
// If no selection or range, exit the function
85+
return;
86+
}
87+
88+
// Get the first range of the selection
89+
// A user can normally only select one range at a time, so the rangeCount will usually be 1
90+
const range = selection.getRangeAt(0);
91+
92+
// If the common ancestor container of the range is the document itself,
93+
// it might mean that the editable element is getting removed from the DOM
94+
// In such cases, especially in Safari, trying to modify the range might throw a HierarchyRequest error
95+
if (range.commonAncestorContainer === document) {
96+
return;
10097
}
98+
99+
// Create a temporary span element to use as an anchor for scrolling
100+
// We can't use the anchor node directly because if it's a Text node, calling scrollIntoView() on it will throw an error
101+
const tempElement = document.createElement('span');
102+
// Collapse the range to its end point
103+
// This means the start and end points of the range will be the same, and it will not contain any content
104+
range.collapse(false);
105+
// Insert the temporary element at the cursor's position at the end of the range
106+
range.insertNode(tempElement);
107+
108+
// Scroll the temporary element into view
109+
// the element will be aligned at the center of the scroll container, otherwise, text and images may be positioned incorrectly
110+
tempElement.scrollIntoView({
111+
block: 'center'
112+
});
113+
tempElement.remove();
101114
}
102115
};

0 commit comments

Comments
 (0)