Home > Mobile >  After calling focus on a given element the selection range's getClientRects will return an empt
After calling focus on a given element the selection range's getClientRects will return an empt

Time:02-02

I'm using the following code to select the element

const selectEnd = (element: HTMLElement, position: number) => {
  element.focus();
  
  const range = document.createRange();
  const sel = window.getSelection();

  if (position !== -1) {
    range.setStart(element, Math.min(position, element.childNodes.length));
  } else {
    range.setStart(element, element.childNodes.length);
  }

  sel?.removeAllRanges();
  sel?.addRange(range);
};

After selecting the element the following rect variable will always be undefined

const selection = window.getSelection();

if (!selection) return;

const range = selection.getRangeAt(0).cloneRange();
range.collapse(true);

const rect = range.getClientRects()[0];

But after an onInput event happens on the client the range.getClientRects function will return a value.

I'm trying to get range.getClientRects() to return a variable after the focus call without the user needing to input anything.

CodePudding user response:

You felt on CSSWG#2514.

The problem is that browsers aren't able to calculate the DOMRects composing a collapsed Range which is anchored to an Element (instead of a Node).

This CKEditor issue lists a few cases where this can happen, along with a few potential workarounds.

1. A collapsed Range in an Element that does have content. (Most probably your case)

Setting the Range on the closest TextNode would allow the browsers to calculate the boxes correctly:

const el = document.querySelector("div");
const range = new Range();
// to show the "bug"
range.selectNode(el);
range.collapse(true);
console.log("selecting the Element", range.getClientRects().length); // 0

// the workaround
range.setStart(el.firstChild, 0);
range.collapse(true);
console.log("selecting the Node", range.getClientRects().length); // 1
<div>target</div>

Though i's also possible that the element's firstChild isn't a TextNode either, so you actually need a method to retrieve the first TextNode in that Element:

function getFirstTextNode(target) {
  return document.createNodeIterator(target, NodeFilter.SHOW_TEXT).nextNode();  
}
const el = document.querySelector("div");
const range = new Range();
// to show how our previous version fails
range.setStart(el.firstChild, 0);
range.collapse(true);
console.log("selecting the firstChild", range.getClientRects().length); // 0

// the workaround
const firstNode = getFirstTextNode(el);
range.setStart(firstNode, 0);
range.collapse(true);
console.log("selecting the first TextNode", range.getClientRects().length); // 1
<div><span></span>target</div>

2. A Range in an empty Element, (and thus without TextNode to select)

For this one you can try appending a Zero-Width-Space (ZWSP) character inside the Element, take your measure on the newly created TextNode, remove the character.

function getFirstTextNode(target) {
  return document.createNodeIterator(target, NodeFilter.SHOW_TEXT).nextNode();  
}
const el = document.querySelector("div");
const range = new Range();
let firstTextNode = getFirstTextNode(el);
const wasEmpty = !firstTextNode;

if (wasEmpty) {
  console.log("inserting a ZWSP");
  firstTextNode = new Text("\u200B");
  el.append(firstTextNode);
}
range.setStart(firstTextNode, 0);
range.collapse(true);
const rect = range.getClientRects()[0];
if (wasEmpty) {
  firstTextNode.remove();
}
console.log(rect); // DOMRect
<div></div>

3. A Range that sits in-between two elements

...</div>|<div>...

Here again you can use the ZWSP trick. Detecting this might become a bit more terse though, and given OP apparently wants to set the caret at the beginning of the Element, that doesn't seem like a possibility for them. So I'll leave the example off from this answer.

  • Related