Даже при использовании 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>