- From: Dan Clark <notifications@github.com>
- Date: Fri, 26 Sep 2025 10:42:23 -0700
- To: whatwg/dom <dom@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <whatwg/dom/pull/1404/review/3269711270@github.com>
@dandclark commented on this pull request. Great to see a spec for this coming together! > @@ -9892,6 +9892,126 @@ and {{Range/getBoundingClientRect()}} methods are defined in other specification [[CSSOM-VIEW]] +<h3 id=interface-formcontrolrange>Interface {{FormControlRange}}</h3> + +<pre class=idl> +[Exposed=Window] +interface FormControlRange : AbstractRange { + constructor(); + + undefined setFormControlRange((HTMLInputElement or HTMLTextAreaElement) element, This is probably the wrong place to bikeshed this name but the more I look at it the more I don't like that we say "FormControlRange" both in the name of the class and the name of this method. At some point we should make space for a proper bikeshedding discussion on this method name, considering other things like `setInFormControl`, `setFormControl`, `setControl`, `setStartAndEnd` or simply `set`. > +[Exposed=Window] +interface FormControlRange : AbstractRange { + constructor(); + + undefined setFormControlRange((HTMLInputElement or HTMLTextAreaElement) element, + unsigned long startOffset, + unsigned long endOffset); + + DOMRectList getClientRects(); + DOMRect getBoundingClientRect(); + stringifier; +}; +</pre> + +<p>Objects implementing the {{FormControlRange}} interface are known as +<dfn export id=concept-live-form-control-range>live FormControlRanges</dfn>.</p> I don't think it's necessary to use "live" in the name like this. Yes, they are 'live', but it doesn't need to be part of the name, we can make sure that's understood from the algorithms and maybe from an editorial note. I know that at https://dom.spec.whatwg.org/#concept-live-range "live" is used, but that's necessary only because the "live" range's interface name is just `Range`, so there's a need to put an extra word in there to disambiguate it from AbstractRange and StaticRange. FormControlRange doesn't have that problem. > +{{AbstractRange/collapsed}} is true if and only if the two offsets are equal.</p> + +<p>An {{Element}} <var>el</var> supports form control ranges if it is an {{HTMLTextAreaElement}}, +or an {{HTMLInputElement}} whose type +<a href="https://html.spec.whatwg.org/multipage/input.html#do-not-apply">supports the selection APIs</a>: +"<code>text</code>", "<code>search</code>", "<code>tel</code>", "<code>url</code>", or +"<code>password</code>".</p> + +<p>For a supported host element <var>el</var>, its <i>value string</i> is the same string exposed by +its IDL attribute {{HTMLTextAreaElement/value}}/{{HTMLInputElement/value}}. Offsets for +{{FormControlRange}} are indices into this string in the inclusive range +[0, <code>value.length</code>], matching +{{HTMLTextAreaElement/selectionStart}}/{{HTMLTextAreaElement/selectionEnd}} and +{{HTMLInputElement/selectionStart}}/{{HTMLInputElement/selectionEnd}} units.</p> + +<p>The <dfn constructor for=FormControlRange lt="FormControlRange()"> could this be put first to match the order of appearance in the IDL? > +}; +</pre> + +<p>Objects implementing the {{FormControlRange}} interface are known as +<dfn export id=concept-live-form-control-range>live FormControlRanges</dfn>.</p> + +<dl class=domintro> + <dt><code><var ignore>formControlRange</var> = new + <a constructor lt="FormControlRange()">FormControlRange()</a></code> + <dd>Returns a new <a>live FormControlRange</a> that tracks text offsets in a text control’s + value as it changes.</dd> + + <dt><code><var ignore>formControlRange</var> . + {{FormControlRange/setFormControlRange()}} (<var ignore>element</var>, + <var ignore>startOffset</var>, <var ignore>endOffset</var>)</code> + <dd>Sets the endpoints to <code>[start, end)</code> within <var>element</var>’s Should `start` and `end` here be `startOffset` and `endOffset`? I'm also not sure about the use of the `[` and `)`. The endOffset can be equal to the length of the value, not just strictly less than. > +host text control’s <i>value string</i> rather than in the <a>node tree</a>. Its +<a for=range>start node</a> and <a for=range>end node</a> are always the host +<code><input></code> or <code><textarea></code>, and its offsets are indices into that +string.</p> + +<p>A {{FormControlRange}} has associated state:</p> +<ul> + <li><dfn export for=FormControlRange id=formcontrolrange-control>control</dfn> + (null or an {{HTMLInputElement}}/{{HTMLTextAreaElement}})</li> + <li><dfn export for=FormControlRange id=formcontrolrange-start>start offset</dfn> + (a non-negative integer)</li> + <li><dfn export for=FormControlRange id=formcontrolrange-end>end offset</dfn> + (a non-negative integer)</li> +</ul> + +<p>The {{AbstractRange/startContainer}} and {{AbstractRange/endContainer}} getters of a I think it is clearer to define these things as a series of getter steps. See the examples from AbstractRange: ``` The startContainer getter steps are to return this’s start node. The startOffset getter steps are to return this’s start offset. The endContainer getter steps are to return this’s end node. The endOffset getter steps are to return this’s end offset. The collapsed getter steps are to return true if this is collapsed; otherwise false. ``` > +<ul> + <li><dfn export for=FormControlRange id=formcontrolrange-control>control</dfn> + (null or an {{HTMLInputElement}}/{{HTMLTextAreaElement}})</li> + <li><dfn export for=FormControlRange id=formcontrolrange-start>start offset</dfn> + (a non-negative integer)</li> + <li><dfn export for=FormControlRange id=formcontrolrange-end>end offset</dfn> + (a non-negative integer)</li> +</ul> + +<p>The {{AbstractRange/startContainer}} and {{AbstractRange/endContainer}} getters of a +{{FormControlRange}} return its <a for=FormControlRange>control</a>. The +{{AbstractRange/startOffset}} and {{AbstractRange/endOffset}} getters return its +<a for=FormControlRange>start offset</a> and <a for=FormControlRange>end offset</a>. +{{AbstractRange/collapsed}} is true if and only if the two offsets are equal.</p> + +<p>An {{Element}} <var>el</var> supports form control ranges if it is an {{HTMLTextAreaElement}}, ```suggestion <p>An {{Element}} <var>el</var> <dfn>supports form control range</dfn> if it is an {{HTMLTextAreaElement}}, ``` Let's make this a formal linkable definition. > + (a non-negative integer)</li> +</ul> + +<p>The {{AbstractRange/startContainer}} and {{AbstractRange/endContainer}} getters of a +{{FormControlRange}} return its <a for=FormControlRange>control</a>. The +{{AbstractRange/startOffset}} and {{AbstractRange/endOffset}} getters return its +<a for=FormControlRange>start offset</a> and <a for=FormControlRange>end offset</a>. +{{AbstractRange/collapsed}} is true if and only if the two offsets are equal.</p> + +<p>An {{Element}} <var>el</var> supports form control ranges if it is an {{HTMLTextAreaElement}}, +or an {{HTMLInputElement}} whose type +<a href="https://html.spec.whatwg.org/multipage/input.html#do-not-apply">supports the selection APIs</a>: +"<code>text</code>", "<code>search</code>", "<code>tel</code>", "<code>url</code>", or +"<code>password</code>".</p> + +<p>For a supported host element <var>el</var>, its <i>value string</i> is the same string exposed by ```suggestion <p>For a <a lt="supports form control range">supported</a>host element <var>el</var>, its <i>value string</i> is the same string exposed by ``` > +{{FormControlRange}} are indices into this string in the inclusive range +[0, <code>value.length</code>], matching +{{HTMLTextAreaElement/selectionStart}}/{{HTMLTextAreaElement/selectionEnd}} and +{{HTMLInputElement/selectionStart}}/{{HTMLInputElement/selectionEnd}} units.</p> + +<p>The <dfn constructor for=FormControlRange lt="FormControlRange()"> +<code>new FormControlRange()</code></dfn> constructor steps are to set <a>this</a>’s +<a for=FormControlRange>control</a> to null and its +<a for=FormControlRange>start offset</a> and <a for=FormControlRange>end offset</a> to 0.</p> + +<p>The <dfn method for=FormControlRange> +<code>setFormControlRange(<var>element</var>, <var>start</var>, <var>end</var>)</code></dfn> +method steps are:</p> + +<ol> + <li>If <var>element</var> does not support form control ranges, then <a>throw</a> a ```suggestion <li>If <var>element</var> does not <a>support form control range</a>, then <a>throw</a> a ``` > + unsigned long startOffset, + unsigned long endOffset); + + DOMRectList getClientRects(); + DOMRect getBoundingClientRect(); + stringifier; +}; +</pre> + +<p>Objects implementing the {{FormControlRange}} interface are known as +<dfn export id=concept-live-form-control-range>live FormControlRanges</dfn>.</p> + +<dl class=domintro> + <dt><code><var ignore>formControlRange</var> = new + <a constructor lt="FormControlRange()">FormControlRange()</a></code> + <dd>Returns a new <a>live FormControlRange</a> that tracks text offsets in a text control’s nit: please replace instances of `’` with `'`. Edge's find-on-page treats these as the same character so you might need to look in your code editor to see what I mean. > +<code>new FormControlRange()</code></dfn> constructor steps are to set <a>this</a>’s +<a for=FormControlRange>control</a> to null and its +<a for=FormControlRange>start offset</a> and <a for=FormControlRange>end offset</a> to 0.</p> + +<p>The <dfn method for=FormControlRange> +<code>setFormControlRange(<var>element</var>, <var>start</var>, <var>end</var>)</code></dfn> +method steps are:</p> + +<ol> + <li>If <var>element</var> does not support form control ranges, then <a>throw</a> a + "{{NotSupportedError!!exception}}" {{DOMException}}.</li> + <li>Let <var>len</var> be the length of <var>element</var>’s <i>value string</i>.</li> + <li>If <var>start</var> > <var>len</var> or <var>end</var> > <var>len</var>, then + <a>throw</a> an "{{IndexSizeError!!exception}}" {{DOMException}}.</li> + <li>If <var>start</var> > <var>end</var>, then set <var>end</var> to <var>start</var>.</li> + <li>Set <a for=FormControlRange>control</a> to <var>element</var>, ```suggestion <li>Set <a>this</a>'s <a for=FormControlRange>control</a> to <var>element</var>, ``` > +<a for=FormControlRange>control</a> to null and its +<a for=FormControlRange>start offset</a> and <a for=FormControlRange>end offset</a> to 0.</p> + +<p>The <dfn method for=FormControlRange> +<code>setFormControlRange(<var>element</var>, <var>start</var>, <var>end</var>)</code></dfn> +method steps are:</p> + +<ol> + <li>If <var>element</var> does not support form control ranges, then <a>throw</a> a + "{{NotSupportedError!!exception}}" {{DOMException}}.</li> + <li>Let <var>len</var> be the length of <var>element</var>’s <i>value string</i>.</li> + <li>If <var>start</var> > <var>len</var> or <var>end</var> > <var>len</var>, then + <a>throw</a> an "{{IndexSizeError!!exception}}" {{DOMException}}.</li> + <li>If <var>start</var> > <var>end</var>, then set <var>end</var> to <var>start</var>.</li> + <li>Set <a for=FormControlRange>control</a> to <var>element</var>, + <a for=FormControlRange>start offset</a> to <var>start</var>, and ```suggestion <a>this</a>'s <a for=FormControlRange>start offset</a> to <var>start</var>, and ``` > +<a for=FormControlRange>start offset</a> and <a for=FormControlRange>end offset</a> to 0.</p> + +<p>The <dfn method for=FormControlRange> +<code>setFormControlRange(<var>element</var>, <var>start</var>, <var>end</var>)</code></dfn> +method steps are:</p> + +<ol> + <li>If <var>element</var> does not support form control ranges, then <a>throw</a> a + "{{NotSupportedError!!exception}}" {{DOMException}}.</li> + <li>Let <var>len</var> be the length of <var>element</var>’s <i>value string</i>.</li> + <li>If <var>start</var> > <var>len</var> or <var>end</var> > <var>len</var>, then + <a>throw</a> an "{{IndexSizeError!!exception}}" {{DOMException}}.</li> + <li>If <var>start</var> > <var>end</var>, then set <var>end</var> to <var>start</var>.</li> + <li>Set <a for=FormControlRange>control</a> to <var>element</var>, + <a for=FormControlRange>start offset</a> to <var>start</var>, and + <a for=FormControlRange>end offset</a> to <var>end</var>.</li> ```suggestion <a>this</a>'s <a for=FormControlRange>end offset</a> to <var>end</var>.</li> ``` > + <li>Edits overlapping the range: If a boundary falls inside text that was removed, + move it to the start of the change. If the edit also inserted new text, remap the boundary + into the inserted span at the closest corresponding offset, not exceeding its end.</li> + <li>Insertion at the start boundary: A non-{{AbstractRange/collapsed}} range expands + to include the new text. A collapsed range (caret) moves after the insertion.</li> + <li>Insertion at the end boundary: A non-collapsed range does not expand to include + the new text. A collapsed range moves after the insertion.</li> + <li>Clamping and collapse: Offsets are clamped to the current value length. If the + {{AbstractRange/startOffset}} would exceed the {{AbstractRange/endOffset}}, set the end to the + start.</li> +</ul> + +<p>The <dfn export for=FormControlRange id=dom-formcontrolrange-stringifier>stringification behavior</dfn> must run these steps:</p> + +<ol> + <li><p>If <a for=FormControlRange>control</a> is null, then return the empty string.</p> ```suggestion <li><p>If <a>this</a>'s <a for=FormControlRange>control</a> is null, then return the empty string.</p> ``` > + move it to the start of the change. If the edit also inserted new text, remap the boundary + into the inserted span at the closest corresponding offset, not exceeding its end.</li> + <li>Insertion at the start boundary: A non-{{AbstractRange/collapsed}} range expands + to include the new text. A collapsed range (caret) moves after the insertion.</li> + <li>Insertion at the end boundary: A non-collapsed range does not expand to include + the new text. A collapsed range moves after the insertion.</li> + <li>Clamping and collapse: Offsets are clamped to the current value length. If the + {{AbstractRange/startOffset}} would exceed the {{AbstractRange/endOffset}}, set the end to the + start.</li> +</ul> + +<p>The <dfn export for=FormControlRange id=dom-formcontrolrange-stringifier>stringification behavior</dfn> must run these steps:</p> + +<ol> + <li><p>If <a for=FormControlRange>control</a> is null, then return the empty string.</p> + <li><p>Let <var>value</var> be <a for=FormControlRange>control</a>'s <i>value string</i>.</p> ```suggestion <li><p>Let <var>value</var> be <a>this</a>'s <a for=FormControlRange>control</a>'s <i>value string</i>.</p> ``` > + into the inserted span at the closest corresponding offset, not exceeding its end.</li> + <li>Insertion at the start boundary: A non-{{AbstractRange/collapsed}} range expands + to include the new text. A collapsed range (caret) moves after the insertion.</li> + <li>Insertion at the end boundary: A non-collapsed range does not expand to include + the new text. A collapsed range moves after the insertion.</li> + <li>Clamping and collapse: Offsets are clamped to the current value length. If the + {{AbstractRange/startOffset}} would exceed the {{AbstractRange/endOffset}}, set the end to the + start.</li> +</ul> + +<p>The <dfn export for=FormControlRange id=dom-formcontrolrange-stringifier>stringification behavior</dfn> must run these steps:</p> + +<ol> + <li><p>If <a for=FormControlRange>control</a> is null, then return the empty string.</p> + <li><p>Let <var>value</var> be <a for=FormControlRange>control</a>'s <i>value string</i>.</p> + <li><p>Let <var>start</var> be <a for=FormControlRange>start offset</a>, and let <var>end</var> be min(<a for=FormControlRange>end offset</a>, <var>value</var>.<code>length</code>).</p> ```suggestion <li><p>Let <var>start</var> be <a>this</a>'s <a for=FormControlRange>start offset</a>, and let <var>end</var> be min(<a>this</a>'s <a for=FormControlRange>end offset</a>, <var>value</var>.<code>length</code>).</p> ``` > + into the inserted span at the closest corresponding offset, not exceeding its end.</li> + <li>Insertion at the start boundary: A non-{{AbstractRange/collapsed}} range expands + to include the new text. A collapsed range (caret) moves after the insertion.</li> + <li>Insertion at the end boundary: A non-collapsed range does not expand to include + the new text. A collapsed range moves after the insertion.</li> + <li>Clamping and collapse: Offsets are clamped to the current value length. If the + {{AbstractRange/startOffset}} would exceed the {{AbstractRange/endOffset}}, set the end to the + start.</li> +</ul> + +<p>The <dfn export for=FormControlRange id=dom-formcontrolrange-stringifier>stringification behavior</dfn> must run these steps:</p> + +<ol> + <li><p>If <a for=FormControlRange>control</a> is null, then return the empty string.</p> + <li><p>Let <var>value</var> be <a for=FormControlRange>control</a>'s <i>value string</i>.</p> + <li><p>Let <var>start</var> be <a for=FormControlRange>start offset</a>, and let <var>end</var> be min(<a for=FormControlRange>end offset</a>, <var>value</var>.<code>length</code>).</p> Elsewhere we ensure that `start offset` <= `end offset` and `end offset` <= `value.length`, so is it necessary to check for these things in the stringifier? > +<code>setFormControlRange(<var>element</var>, <var>start</var>, <var>end</var>)</code></dfn> +method steps are:</p> + +<ol> + <li>If <var>element</var> does not support form control ranges, then <a>throw</a> a + "{{NotSupportedError!!exception}}" {{DOMException}}.</li> + <li>Let <var>len</var> be the length of <var>element</var>’s <i>value string</i>.</li> + <li>If <var>start</var> > <var>len</var> or <var>end</var> > <var>len</var>, then + <a>throw</a> an "{{IndexSizeError!!exception}}" {{DOMException}}.</li> + <li>If <var>start</var> > <var>end</var>, then set <var>end</var> to <var>start</var>.</li> + <li>Set <a for=FormControlRange>control</a> to <var>element</var>, + <a for=FormControlRange>start offset</a> to <var>start</var>, and + <a for=FormControlRange>end offset</a> to <var>end</var>.</li> +</ol> + +<p>A {{FormControlRange}} is <em>live</em>: when the control’s value string changes, the range’s An interesting case I thought of is what happens if sets a FormControlRange on an `<input>` and then the `<input>`'s `type` is changed to something that doesn't support `FormControlRange`, like `date`? Maybe the FormControlRange's `control` should get set to null when that happens? > + "{{NotSupportedError!!exception}}" {{DOMException}}.</li> + <li>Let <var>len</var> be the length of <var>element</var>’s <i>value string</i>.</li> + <li>If <var>start</var> > <var>len</var> or <var>end</var> > <var>len</var>, then + <a>throw</a> an "{{IndexSizeError!!exception}}" {{DOMException}}.</li> + <li>If <var>start</var> > <var>end</var>, then set <var>end</var> to <var>start</var>.</li> + <li>Set <a for=FormControlRange>control</a> to <var>element</var>, + <a for=FormControlRange>start offset</a> to <var>start</var>, and + <a for=FormControlRange>end offset</a> to <var>end</var>.</li> +</ol> + +<p>A {{FormControlRange}} is <em>live</em>: when the control’s value string changes, the range’s +{{AbstractRange/startOffset}} and {{AbstractRange/endOffset}} are updated automatically +to preserve the same logical content. These behaviors mirror {{Range}} boundary adjustments +in the DOM, but are applied to the UTF-16 code units of a form control’s value.</p> + +<ul> This is good as a placeholder and/or as a note to explain how the range gets updated when the `value` changes. To really be complete, we probably need to make changes against the HTML spec to define this behavior. That would probably look like defining an algorithm taking parameters describing the value change that handles the boundary adjustments for each FormControlRange associated with the control, and then finding all the places in the HTML spec where these controls change their values and calling that algorithm in each of those places. -- Reply to this email directly or view it on GitHub: https://github.com/whatwg/dom/pull/1404#pullrequestreview-3269711270 You are receiving this because you are subscribed to this thread. Message ID: <whatwg/dom/pull/1404/review/3269711270@github.com>
Received on Friday, 26 September 2025 17:42:28 UTC