Skip to content

Add getRangeFromValue() explainer #1075

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

t-andresre
Copy link
Contributor

This PR adds the getRangeFromValue() explainer & issue template, updates the README.md file.

@t-andresre
Copy link
Contributor Author

@t-andresre please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@microsoft-github-policy-service agree


## Proposed Approach

The `getRangeFromValue()` API will create a Range representing the `value` of a `<textarea>` or `<input>` element. It accepts two parameters: start_offset and end_offset. These parameters determine the start and end nodes, and their offsets, within the Range object, considering the specific DOM structure of these elements. In an `<input>`, the range is always within a single text node holding the value. In contrast, a `<textarea>` may have multiple text nodes if line breaks are present, potentially resulting in different start and end nodes. Importantly, the element itself will never be the start or end node.
Copy link
Member

Choose a reason for hiding this comment

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

It seems that it's already possible to get a Range pointing to text inside a <textarea>. For example:
https://codepen.io/daniec/pen/xbwKPzz?editors=1000
When calling getBoundingClientRect() on such a range, the DOMRect that's returned is empty at 0, 0.

So it seems there are two possibilities here to to make Use Case 1 work.

  • We change the behavior of Range such that any range placed on text inside a text area returns the actual coordinates when getBoundingClientRect() is called. But then getRangeFromValue() is not really needed for <textarea>, only for <input>.
  • Make the Ranges returned from getRangeFromValue() "special" in that they can return actual coordinates for getBoundingClientRect() in a textarea. But this seems potentially confusing. Would this specialness be exposed to JavaScript in any kind of explicit way?

And do we have the same issue for using Ranges in a <textarea> to draw custom highlights?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Creating a Range that targets text inside a <textarea> is only possible by referencing its DOM text node. This node exists only if the <textarea> has a default value set in the HTML—like in your CodePen example:

<textarea>We are trying something out here</textarea>

However, there's an important caveat:


  • If the user hasn’t typed anything into the field (in other words, there has been no user interaction), the textarea.value and its text node (textarea.firstChild.nodeValue) will be synchronized. Meaning, updating the text node like this: textarea.firstChild.nodeValue = "Arbitrary Value"; will also update the value of the <textarea>, as well as what is displayed in the field itself.

image

Demo Sequence for this CodePen:

Display Text → Display Value → Change Text Node Through JS → Display Text → Display Value


  • However, once the user types into the field, the synchronization between the text node and the field value will break. The value attribute will reflect the user’s input, but the text node will remain unchanged. This means that if you check textarea.firstChild.nodeValue, after manually editing the field, it will still show the original HTML value. In addition, the text rendered on the field will no longer match the text node in the DOM.

image

Demo sequence in the CodePen:
Display Text → Display Value → Manually Change Textarea Input → Display Text → Display Value

Once it gets to this point, modifying the text node via JavaScript won’t affect what’s rendered or the value property. Even clearing the field manually (through backspace) won’t restore the sync.

And so, while it’s technically possible to create a Range inside a <textarea>, its usefulness is very limited due to this behavior. The <textarea> would need to be static and not accept user input, which in most use cases would defeat the purpose of using an input field altogether.

This behavior also applies to <input> elements.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for that explanation! That's interesting, I didn't know that the synchronization breaks once the user has edited the field.

Isn't this also a problem for getRangeFromValue() though? Once synchronization has broken, what is the startContainer/endContainer of the Range that getRangeFromValue() returns?


## Proposed Approach

The `getRangeFromValue()` API will create a Range representing the `value` of a `<textarea>` or `<input>` element. It accepts two parameters: start_offset and end_offset. These parameters determine the start and end nodes, and their offsets, within the Range object, considering the specific DOM structure of these elements. In an `<input>`, the range is always within a single text node holding the value. In contrast, a `<textarea>` may have multiple text nodes if line breaks are present, potentially resulting in different start and end nodes. Importantly, the element itself will never be the start or end node.
Copy link
Member

Choose a reason for hiding this comment

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

For the <input> case I'd like to understand more about the Text node that will be the Range's startContainer/endContainer. Currently no Text node is exposed to script for an <input> element, so this would be something new. Several questions come to mind:

  • Is this Text Node connected to the DOM in any way? Or does it have no parent?
  • What happens when this Text Node is moved or modified? Are those changes reflected in the <input>?
  • Are changes to the <input> reflected in the Text node? Or is a new Text node created?
  • Should we provide a more direct way to get this special Text node from an <input>?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're absolutely right: currently, no text node is exposed to scripts for <input> elements. However, that only applies to the Light DOM, because both <input> and <textarea> elements rely on internal text nodes for rendering, hidden within the Shadow DOM. These are the text nodes that are used to create the Range object through getRangeFromValue(). Since they are used for rendering, every input change made to an <input> or <textarea> element updates the nodes accordingly. The same holds true for the other way around, albeit not something that can be currently done on web developers' end, as far as I am aware. One of our proposals before getRangeFromValue() was to expose these text nodes, and have them be used to manually create a Range on the web developers' end, something like this:

const range = new Range();
range.setStart(inputInternalTextNode, 0);
range.setStart(inputInternalTextNode, 6);

However, these text nodes are hidden in the Shadow DOM for optimization purposes, and exposing them as they are in the Light DOM would entail modifying that logic and handling performance implications.

Copy link
Member

Choose a reason for hiding this comment

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

Summarizing discussion from our call earlier today:

  • We can't do anything that would expose nodes from inside a <textarea> or <input> shadow DOM to script on a webpage.
  • So @t-andresre will spend some time exploring whether a special Range type could be invented that would satisfy these use cases without actually exposing the Text node.
  • Based on the result of that investigation, we'll decide whether to move forward with that approach or go back to the more scoped getSelectionBoundingClientRect proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants