Неправильное позиционирование getBoundingClientRect после символа новой строки - PullRequest
0 голосов
/ 16 января 2020

Я пытаюсь заставить выпадающее меню следовать за курсором в Rich Text Editor для Интернета. Используя следующее, я могу получить координаты курсора без проблем:

const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) return;
const position = sel.getRangeAt(0).getBoundingClientRect());

Однако, если я попытаюсь использовать это после символа \n, он вернет позицию курсора после символа новой строки, а не чем начало новой строки (где курсор фактически появляется в окне): enter image description here

Есть ли способ избежать этого?

Редактировать: На основе в комментариях ниже приведена более подробная версия того, чего я пытаюсь достичь.

В настоящее время я создаю текстовый редактор с помощью React и Slate. js (https://github.com/ianstormtaylor/slate). Это более надежная версия компонента contentEditable по своей сути, но позволяет вам перетаскивать редактируемое текстовое поле на страницу. Из-за структуры узла, который я использую, я хочу, чтобы между абзацами были мягкие разрывы, а не новые элементы <div />. Поскольку это нестандартное поведение для contentEditable, очень сложно создать небольшой пример без воссоздания всего приложения.

Редактирование (дальнейшие ответы на комментарии): необработанный HTML текстового элемента выглядит следующим образом это:

<span data-slate-string="true">working until newline
see?
</span>

вы можете видеть, что сланец буквально переводит разрыв в \ n символ, который, как я думаю, вызывает проблему.

1 Ответ

1 голос
/ 17 января 2020

Даже при использовании contenteditable по умолчанию браузера действительно странное поведение, когда курсор установлен на новую строку: getClientRects() диапазона будет пустым и, таким образом, getBoundingClientRect() вернет полный 0 DOMRect.

Вот простая демонстрация, демонстрирующая проблему:

const target = document.getElementById('target');

document.onselectionchange = (e) => {
  const sel = window.getSelection();
  if (!sel || sel.rangeCount === 0) {
    return;
  }
  const range = sel.getRangeAt(0);
  const position = range.getBoundingClientRect();

  floater.style.top = position.bottom + 'px';
  floater.style.left = position.right + 'px';
}
#floater {
  position: absolute;
  width: 20px;
  height: 30px;
  background: #DDAADDCC;
  pointer-events: none;
  bottom: 0;
}
<div id="target" contenteditable>Type here and enter new lines</div>
<div id="floater"></div>

Для этого существует простой обходной путь, заключающийся в выборе содержимого контейнера текущего диапазона:

// check if we have client rects
const rects = range.getClientRects();
if(!rects.length) {
  // probably new line buggy behavior
  if(range.startContainer && range.collapsed) {
    // explicitely select the contents
    range.selectNodeContents(range.startContainer);
  }
}

const target = document.getElementById('target');

document.onselectionchange = (e) => {
  const sel = window.getSelection();
  if (!sel || sel.rangeCount === 0) {
    return;
  }
  const range = sel.getRangeAt(0);
  
  // check if we have client rects
  const rects = range.getClientRects();
  if(!rects.length) {
    // probably new line buggy behavior
    if(range.startContainer && range.collapsed) {
      // explicitely select the contents
      range.selectNodeContents(range.startContainer);
    }
  }
  
  const position = range.getBoundingClientRect();

  floater.style.top = position.bottom + 'px';
  floater.style.left = position.right + 'px';
}
#floater {
  position: absolute;
  width: 20px;
  height: 30px;
  background: #DDAADDCC;
  pointer-events: none;
  bottom: 0;
}
<div id="target" contenteditable>Type here and enter new lines</div>
<div id="floater"></div>

Теперь OP, кажется, в другом вопросе, так как они имеют дело с программными брейками \n и white-space: pre.
Однако я смог воспроизвести его только из моего Firefox. , Chrome, который в этом случае вел себя "как положено" ...

Так что в моем Firefox, DOMRect не будет все 0, но это будет тот, что до разрыва строки.

Чтобы продемонстрировать этот случай, нажмите на пустую строку:

const target = document.getElementById('target');

document.onselectionchange = (e) => {
  const sel = window.getSelection();
  if (!sel || sel.rangeCount === 0) {
    return;
  }
  const range = sel.getRangeAt(0);
  const position = range.getBoundingClientRect();

  floater.style.top = position.bottom + 'px';
  floater.style.left = position.right + 'px';
}
#target {
  white-space: pre;
}
#floater {
  position: absolute;
  width: 20px;
  height: 30px;
  background: #DDAADDCC;
  pointer-events: none;
  bottom: 0;
}
<div id="target" contenteditable>Click on the below empty line

Click on the above empty line</div>
<div id="floater"></div>

И чтобы обойти этот случай, он немного сложнее ...
Нам нужно проверить, что символ перед нашим Range, если это новая строка, то нам нужно обновить наш диапазон, выбрав следующий символ. Но при этом мы бы также переместили курсор, поэтому нам действительно нужно сделать это из клонированного Range. Но поскольку Chrome ведет себя не так, нам нужно также проверить, был ли предыдущий символ в другой строке, что становится проблемой, когда такого предыдущего символа нет ...

const target = document.getElementById('target');

document.onselectionchange = (e) => {
  const sel = window.getSelection();
  if (!sel || sel.rangeCount === 0) {
    return;
  }
  const range = sel.getRangeAt(0);
  // we can still workaround the default behavior too
  const rects = range.getClientRects();
  if(!rects.length) {
    if(range.startContainer && range.collapsed) {
      range.selectNodeContents(range.startContainer);
    }
  }

  let position = range.getBoundingClientRect();
  const char_before = range.startContainer.textContent[range.startOffset - 1];
  // if we are on a \n
  if(range.collapsed && char_before === "\n") {
    // create a clone of our Range so we don't mess with the visible one
    const clone = range.cloneRange();
    // check if we are experiencing a bug
    clone.setStart(range.startContainer, range.startOffset-1);
    if(clone.getBoundingClientRect().top === position.top) {
      // make it select the next character
      clone.setStart(range.startContainer, range.startOffset + 1 );
      position = clone.getBoundingClientRect();
    }
  }
  
  floater.style.top = position.bottom + 'px';
  floater.style.left = position.right + 'px';
}
#target {
  white-space: pre;
}
#floater {
  position: absolute;
  width: 20px;
  height: 30px;
  background: #DDAADDCC;
  pointer-events: none;
  bottom: 0;
}
<div id="target" contenteditable>Click on the below empty line

Click on the above empty line</div>
<div id="floater"></div>
...