[w3c/editing] Proposal: compositionborder attribute (Issue #414)

The idea is to allow a `compositionborder=true` attribute to elements within a content editable container. This signals to the browser that composition should not cross the element's boundary. When IME composition begins, the initial composition text is bounded by `compositionborder`. Input event Ranges will necessarily be within those bounds. An input event Range outside the range would necessarily trigger a `compositionend` event first.

# Purpose
Composition events cannot be handled manually (specifically `preventDefault` is a not allowed in the InputEvents spec). During composition editing, browsers may add/delete nodes, or move textContent to adjacent nodes. This can cause a "style jank" where a style is modified and we must wait until composition has ended to repair the styles. This is only a problem with styled text, as with plain text we can repair the DOM afterwards without any visible jank. This also only affects compositions, since regular events we can cancel and manually edit the DOM.

Here are some examples of unpredictable browser behavior around compositions. Each of these result in style jank, but could be solved by adding `compositionborder=true` to one or more elements. 

(Vertical bar indicates caret position)

### 1)
```html
the suffix -esque, as in pictur<u>esqu|</u>
```
Autocomplete "picturesque" in Chrome, or typing "e" in Firefox Android will result in:
```html
the suffix -esque, as in <u>picturesque</u>
```
ruining our intended styling.

### 2)
In a code editor, the same might occur with:
```html
<var>let</var> x = <var>shor</var>-<var>ter|</var>
```
The browser does not know that we're dealing with programming code, and so thinks `shor-ter` is a single word/phrase that should be composed together with the IME.

This particular example can be observed in CodeMirror when editing on Android; when typing `this-some_var`, syntax highlighting is not properly applied until after composition finishes.

### 3)
```html
<b>some text </b><u>|</u>
```
Typing "here" will result in
```html
<b>some text here|</b><u></u>
```
The browser treats the contents as one style-agnostic text phrase, ignoring that the cursor is inside `<u>` and retargeting it to `<b>` instead. While normal input events we can cancel and retarget it back to `<u>`, with composition events we can't edit the DOM until after composition has completed; and by that point, it is too late, since the text has already been styled bold.

### 4)
```html
<template id="custom-element">
   <img src="avatar.png>
   <b>~ <slot></slot> ~</b>
</template>
<custom-element>John Doe</custom-element>is| a programmer from Orlando
```
(Assuming custom-element has a shadow root given by the template). Here, browsers interpret "Doeis" as the phrase for composition, even though the actual text being displayed is "~ John Doe ~ is...". A similar example can be crafted for before/after CSS pseudo elements.

# Usage

Add `compositionborder=true` to an element to restrict IME composition. For example, given a caret given by vertical bar, the brackets indicate the composition bounds:

```html
<div compositionborder=true>[
   <span>foo| bar<span>
   <div>]<span compositionborder=true>baz</span></div>
</div>
```
In this example, the browser would insert text inside `<u>` instead of crossing to another element:
```html
<b>foo</b><u>[|]</u>
```

Using the syntax of [insertAdjacentElement](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement), the composition boundary is the first position preceding/following the anchor whose element compositionborder is true or isContentEditable is false.

Using the node-boundary package, code for the range would look like:
```js
import {Boundary, BoundaryRange} from "node-boundary";

/** Get the range for composition given an anchor point
 * @param {Boundary} anchor - the DOM position whose composition range we want to find
 * @param {Node} [root] a root element that is an additional bounds constraint (e.g. the root
 *  contenteditable); anchor should be inside root
 * @returns {BoundaryRange} a range giving the composition boundary start/end; can be collapsed,
 *  in which case composition would insert a new Text node at that point
 */
function composition_bounds(anchor, root){
 // to detect whether isContentEditable changes, we need the initial state at this anchor
 const is_boundary = n => {
  return n === root || (!(n instanceof CharacterData) && (
   n.compositionBorder === "true" ||
   !n.isContentEditable !== anchor_editable));
 };
 const range = new BoundaryRange();
 // traverse left/right to get bounds
 const lanchor = anchor.clone();
 for (const _ of lanchor.previousNodes()){
  if (is_boundary(lanchor.node))
   break;
 }
 range.start = lanchor;
 const ranchor = anchor.clone();
 for (const _ of ranchor.nextNodes()){
  if (is_boundary(ranchor.node))
   break;
 }
 range.end = ranchor;
}
```

# Implementation

The idea here is a simple change to encapsulate composition events so they are more controllable. While browser devs have made clear full composition handling cannot be supported easily, this should be a simple addition which I believe solves the majority of complaints surrounding composition events. Browsers already seem to implement some logic surrounding this, although it is not exposed; for example, Firefox appears to exhibit `compositionborder=true` behavior for `<article>`.

-- 
Reply to this email directly or view it on GitHub:
https://github.com/w3c/editing/issues/414
You are receiving this because you are subscribed to this thread.

Message ID: <w3c/editing/issues/414@github.com>

Received on Friday, 9 December 2022 01:24:20 UTC