-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[72837] Click position is lost when activating an inline edit field #22232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,7 @@ | |
|
|
||
| import { Controller } from '@hotwired/stimulus'; | ||
| import { renderStreamMessage } from '@hotwired/turbo'; | ||
| import { caretRangeFromPoint } from 'core-app/shared/helpers/ckeditor-helpers'; | ||
|
|
||
| export default class extends Controller { | ||
| static values = { | ||
|
|
@@ -50,6 +51,10 @@ export default class extends Controller { | |
| this.boundFormDataHandler = (e:FormDataEvent) => this.appendStableKeySystemArguments(e); | ||
| form.addEventListener('formdata', this.boundFormDataHandler); | ||
| } | ||
|
|
||
| if (this.element instanceof HTMLInputElement || this.element instanceof HTMLTextAreaElement) { | ||
| this.setCursorPosition(this.element); | ||
| } | ||
| } | ||
|
|
||
| disconnect() { | ||
|
|
@@ -72,6 +77,8 @@ export default class extends Controller { | |
| return; | ||
| } | ||
|
|
||
| this.storeCursorPositionData(e); | ||
|
|
||
| const response = await fetch(this.urlValue, { | ||
| method: 'GET', | ||
| headers: { Accept: 'text/vnd.turbo-stream.html' }, | ||
|
|
@@ -138,4 +145,60 @@ export default class extends Controller { | |
| } | ||
| return false; | ||
| } | ||
|
|
||
| // When the controller is connected to a text input (i.e. the edit field has | ||
| // just been rendered), apply the stored char offset so the cursor lands where | ||
| // the user clicked in the display field. | ||
| private setCursorPosition(element:HTMLElement):void { | ||
| const offset = parseInt(document.body.dataset.inplaceEditCharOffset ?? '', 10); | ||
| delete document.body.dataset.inplaceEditCharOffset; | ||
| if (!isNaN(offset)) { | ||
| // requestAnimationFrame ensures autofocus has run and the element is focused. | ||
| // setSelectionRange is not supported on all input types (e.g. number, date) — | ||
| // those will silently keep the browser's default cursor placement. | ||
| requestAnimationFrame(() => { | ||
| try { | ||
| (element as HTMLInputElement).setSelectionRange(offset, offset); | ||
| } catch { | ||
|
Comment on lines
+152
to
+162
|
||
| // ignore | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| private storeCursorPositionData(e:Event):void { | ||
| if (e instanceof MouseEvent) { | ||
| const container = e.currentTarget as HTMLElement; | ||
| const rect = container.getBoundingClientRect(); | ||
| document.body.dataset.inplaceEditClickX = String(e.clientX - rect.left); | ||
| document.body.dataset.inplaceEditClickY = String(e.clientY - rect.top); | ||
|
|
||
| // For plain-text inputs: store the char offset at the click position so | ||
| // the rendered text input can place the cursor accurately via setSelectionRange. | ||
| const range = caretRangeFromPoint(e.clientX, e.clientY); | ||
| if (range && container.contains(range.startContainer)) { | ||
| document.body.dataset.inplaceEditCharOffset = String( | ||
| this.getCharOffset(container, range.startContainer, range.startOffset), | ||
| ); | ||
| } else { | ||
| delete document.body.dataset.inplaceEditCharOffset; | ||
| } | ||
| } else { | ||
| delete document.body.dataset.inplaceEditClickX; | ||
| delete document.body.dataset.inplaceEditClickY; | ||
| delete document.body.dataset.inplaceEditCharOffset; | ||
| } | ||
| } | ||
|
|
||
| private getCharOffset(root:Element, targetNode:Node, targetOffset:number):number { | ||
| let count = 0; | ||
| let node:Node|null; | ||
| const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); | ||
|
|
||
| while ((node = walker.nextNode())) { | ||
| if (node === targetNode) return count + targetOffset; | ||
| count += (node as Text).length; | ||
| } | ||
| return count; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
document.caretPositionFromPointanddocument.caretRangeFromPointare not part of the standard DOM typings in many TypeScript setups. As written, this will typically fail to compile (Property 'caretPositionFromPoint' does not exist on type 'Document', same forcaretRangeFromPoint). Use a typed cast (e.g.,const doc = document as { caretPositionFromPoint?: ...; caretRangeFromPoint?: ... }) or add a local/global type augmentation so the helper remains type-safe withoutanyerrors.