Skip to content

[72837] Click position is lost when activating an inline edit field#22232

Closed
HDinger wants to merge 1 commit into
devfrom
bug/72837-click-position-is-lost-when-activating-an-inline-edit-field
Closed

[72837] Click position is lost when activating an inline edit field#22232
HDinger wants to merge 1 commit into
devfrom
bug/72837-click-position-is-lost-when-activating-an-inline-edit-field

Conversation

@HDinger
Copy link
Copy Markdown
Contributor

@HDinger HDinger commented Mar 6, 2026

Ticket

https://community.openproject.org/wp/72837

What are you trying to accomplish?

Place cursor at click position when activating inplace edit fields. This works well for normal (text) inputs. Date and number inputs unfortunately don't support the setSelectionRange method, so there was not much I could do for them.
The CkEditor is a bit special. This PR attempts to also remember the click position for this field, but since the formatting between the two views differs so heavily it is basically impossible to set it to the exact same spot.

How it works

  • On click (before the edit field is fetched via Turbo Stream) the click position is captured in two fields on document.body.dataset:

    • Relative coordinates (inplaceEditClickX/Y) — for CKEditor, where the content layout differs between display and editor
    • Character offset (inplaceEditCharOffset) — computed via caretRangeFromPoint on the display field's text nodes, for plain text inputs where the content is identical
  • After the edit field renders:

    • CKEditor: reads the relative coordinates, converts them back to absolute viewport coordinate of the editable element, and positions the CKEditor cursor via its internal model API
    • Text inputs / textareas: reads the character offset and applies it via setSelectionRange.
    • Input types that don't support this (e.g. number, date) fall back to the browser's default cursor placement

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to preserve the user’s click position when activating an inline edit field that renders a CKEditor instance, so the caret is placed where the user clicked.

Changes:

  • Persist the triggering click coordinates on document.body.dataset when starting the Turbo-stream request for the inplace edit form.
  • On CKEditor wrapper connect, read those coordinates and attempt to set the CKEditor selection to the corresponding DOM/model range before focusing.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
frontend/src/stimulus/controllers/dynamic/inplace-edit.controller.ts Stores (and clears) click coordinates on the body dataset when issuing the Turbo-stream fetch for inplace edit.
frontend/src/stimulus/controllers/ckeditor-focus.controller.ts Reads stored click coordinates and tries to translate them into a CKEditor selection before focusing the editor.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread frontend/src/stimulus/controllers/dynamic/inplace-edit.controller.ts Outdated
Comment thread frontend/src/stimulus/controllers/ckeditor-focus.controller.ts
Comment thread frontend/src/stimulus/controllers/ckeditor-focus.controller.ts
Comment thread frontend/src/stimulus/controllers/ckeditor-focus.controller.ts Outdated
Comment thread frontend/src/stimulus/controllers/ckeditor-focus.controller.ts Outdated
Base automatically changed from bug/71863-editor-is-not-focused-when-activating-edit-field to release/17.2 March 9, 2026 07:10
@HDinger HDinger force-pushed the bug/72837-click-position-is-lost-when-activating-an-inline-edit-field branch from 087a884 to d463e59 Compare April 1, 2026 06:48
@HDinger HDinger added this to the 17.3.x milestone Apr 1, 2026
@HDinger HDinger changed the base branch from release/17.2 to release/17.3 April 1, 2026 06:49
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 1, 2026

Dependency Review

The following issues were found:

  • ❌ 3 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 11 package(s) with unknown licenses.
  • ⚠️ 2 packages with OpenSSF Scorecard issues.

View full job summary

@HDinger HDinger force-pushed the bug/72837-click-position-is-lost-when-activating-an-inline-edit-field branch from d463e59 to 8b783cb Compare April 1, 2026 10:47
@HDinger HDinger marked this pull request as ready for review April 1, 2026 10:56
@myabc myabc force-pushed the bug/72837-click-position-is-lost-when-activating-an-inline-edit-field branch from 8b783cb to a175e93 Compare April 22, 2026 16:52
@myabc myabc requested a review from Copilot April 22, 2026 16:52

This comment was marked as outdated.

@myabc myabc changed the base branch from release/17.3 to dev April 22, 2026 16:56
@opf opf deleted a comment from github-actions Bot Apr 22, 2026
@myabc myabc requested review from Copilot and myabc April 22, 2026 17:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment on lines 69 to 78
// Don't trigger edit mode if the user is selecting text or just finished a selection
if (window.getSelection()?.toString()) {
return;
}

// Don't trigger edit mode if clicking on a link
const target = e.target as HTMLElement;
if (target.tagName === 'a' || target.closest('a')) {
return;
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request() returns early (text selection or link click) before calling storeCursorPositionData(), so the document.body.dataset.inplaceEditClickX/Y and inplaceEditCharOffset values from a previous activation can remain and be reused unintentionally. Clear the stored dataset values on these early-return paths (or move storeCursorPositionData() before the guards and have it explicitly reset state when activation is aborted).

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +162
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 {
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setCursorPosition is only called with HTMLInputElement | HTMLTextAreaElement, but the method signature accepts HTMLElement and then type-asserts to HTMLInputElement to call setSelectionRange. Tighten the parameter type to HTMLInputElement | HTMLTextAreaElement (and avoid the cast) so TypeScript can enforce correct usage and autocomplete the proper API surface.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +50
export function caretRangeFromPoint(x:number, y:number):Range|null {
if ('caretPositionFromPoint' in document) {
const pos = document.caretPositionFromPoint(x, y);
if (pos) {
const range = document.createRange();
range.setStart(pos.offsetNode, pos.offset);
range.collapse(true);
return range;
}
return null;
}
return (document as Document).caretRangeFromPoint?.(x, y) ?? null;
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document.caretPositionFromPoint and document.caretRangeFromPoint are 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 for caretRangeFromPoint). 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 without any errors.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@myabc myabc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @HDinger,

I'm not yet done with the review, but some initial findings:

Functionality

I tried the solution with Safari, Firefox and Chrome. The solution seems to work OK across browsers in larger viewports:

https://github.com/user-attachments/assets/0b3feeaa-35b2-41b5-9ecc-54e95f6c7938
https://github.com/user-attachments/assets/ee6e9074-5db6-47b1-bdd7-609d6deb34d2

However it inserts the caret in the wrong place on smaller viewports (in these examples, I tried using the DevTools responsive mode):

https://github.com/user-attachments/assets/18899573-5c12-4b21-a170-2bdc3371f6b9
https://github.com/user-attachments/assets/0e041691-27e3-46da-a894-5c59f6cf2e4d

I'm not sure why this is - perhaps related to display and input text size differing on smaller viewports (which in itself, is somewhat jarring)

I think always inserting the caret at the beginning of the text (i.e. the buggy behaviour) is probably preferable to a solution where insertion is erratic.

Architecture-wise

I really don't like setting data like cursor position coordinates on document.body itself. However, if we do need to use document.body.dataset as some sort of temporary store, then I'd prefer to see an abstraction around it.

I'm still trying to get my head around the controller and its lifecycle, so apologies if the following alternate suggestions are naive or unfeasible:

  • Can we not scope cursor co-ordinates to individual inplace-edit controllers (or at least a parent context)? Stimulus values would provide an idiomatic way of handling this. However if the controller is not disconnected after a Turbo request, this could also just be an instance variable on the controller?
  • Another option would be to send the click coordinates with the HTTP request. Initially I thought this was a bit outlandish, but since we are round-tripping to the server, passing request variables that then then set metadata on the rendered CKEditor might work.

@HDinger HDinger removed this from the 17.3.x milestone May 26, 2026
@HDinger
Copy link
Copy Markdown
Contributor Author

HDinger commented May 29, 2026

Closing in favour of #23449

@HDinger HDinger closed this May 29, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators May 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Development

Successfully merging this pull request may close these issues.

3 participants