В качестве примера у меня есть этот простой HTML:
<div id="editable" contenteditable="true">
text text text<br>
text text text<br>
text text text<br>
</div>
<button id="button">focus</button>
Мне нужна простая вещь - когда я нажимаю кнопку, я хочу поместить курсор (курсор) в определенное место в редактируемом div. После поиска в Интернете у меня есть этот JS, прикрепленный к нажатию кнопки, но он не работает (FF, Chrome):
var range = document.createRange();
var myDiv = document.getElementById("editable");
range.setStart(myDiv, 5);
range.setEnd(myDiv, 5);
Можно ли установить такое положение каретки вручную?
setSelectionRange()
функцию из ответа, который я написал здесь: stackoverflow.com/questions/6240139/… . Как я отметил в ответе, есть разные вещи, которые он не может обрабатывать правильно / последовательно, но может быть достаточно хорош.document.createRange
(илиwindow.getSelection
, но до этого не дойдет).Большинство ответов, которые вы найдете на предметном размещении курсора, довольно упрощены, поскольку они предназначены только для ввода с обычным текстом. Как только вы используете элементы html внутри контейнера, введенный текст разбивается на узлы и свободно распределяется по древовидной структуре.
Чтобы установить положение курсора, у меня есть эта функция, которая обходит все дочерние текстовые узлы в предоставленном узле и устанавливает диапазон от начала начального узла до символа chars.count :
function createRange(node, chars, range) { if (!range) { range = document.createRange() range.selectNode(node); range.setStart(node, 0); } if (chars.count === 0) { range.setEnd(node, chars.count); } else if (node && chars.count >0) { if (node.nodeType === Node.TEXT_NODE) { if (node.textContent.length < chars.count) { chars.count -= node.textContent.length; } else { range.setEnd(node, chars.count); chars.count = 0; } } else { for (var lp = 0; lp < node.childNodes.length; lp++) { range = createRange(node.childNodes[lp], chars, range); if (chars.count === 0) { break; } } } } return range; };
Затем я вызываю процедуру с помощью этой функции:
function setCurrentCursorPosition(chars) { if (chars >= 0) { var selection = window.getSelection(); range = createRange(document.getElementById("test").parentNode, { count: chars }); if (range) { range.collapse(false); selection.removeAllRanges(); selection.addRange(range); } } };
Range.collapse (false) устанавливает курсор в конец диапазона. Я тестировал его с последними версиями Chrome, IE, Mozilla и Opera, и все они работают нормально.
PS. Если кому-то интересно, я получаю текущую позицию курсора, используя этот код:
function isChildOf(node, parentId) { while (node !== null) { if (node.id === parentId) { return true; } node = node.parentNode; } return false; }; function getCurrentCursorPosition(parentId) { var selection = window.getSelection(), charCount = -1, node; if (selection.focusNode) { if (isChildOf(selection.focusNode, parentId)) { node = selection.focusNode; charCount = selection.focusOffset; while (node) { if (node.id === parentId) { break; } if (node.previousSibling) { node = node.previousSibling; charCount += node.textContent.length; } else { node = node.parentNode; if (node === null) { break } } } } } return charCount; };
Код делает противоположность функции set - он получает текущие window.getSelection (). FocusNode и focusOffset и считает в обратном порядке все встречающиеся текстовые символы, пока не попадет в родительский узел с идентификатором containerId. Функция isChildOf перед запуском просто проверяет, является ли указанный узел дочерним по отношению к предоставленному parentId .
Код должен работать прямо без изменений, но я просто взял его с JQuery плагин я разработал так уже вырубил пару этого х - дайте мне знать , если что - то не работает!
источник
node.id
иparentId
к чему, без примера. Спасибо :)const el = document.getElementById("editable"); el.focus() let char = 1, sel; // character at which to place caret if (document.selection) { sel = document.selection.createRange(); sel.moveStart('character', char); sel.select(); } else { sel = window.getSelection(); sel.collapse(el.lastChild, char); }
источник
Если вы не хотите использовать jQuery, вы можете попробовать этот подход:
public setCaretPosition() { const editableDiv = document.getElementById('contenteditablediv'); const lastLine = this.input.nativeElement.innerHTML.replace(/.*?(<br>)/g, ''); const selection = window.getSelection(); selection.collapse(editableDiv.childNodes[editableDiv.childNodes.length - 1], lastLine.length); }
editableDiv
редактируемый элемент, не забудьте установитьid
для него. Затем нужно вытащитьinnerHTML
элемент и перерезать все тормозные магистрали. И просто установите коллапс со следующими аргументами.источник
function set_mouse() { var as = document.getElementById("editable"); el = as.childNodes[1].childNodes[0]; //goal is to get ('we') id to write (object Text) because it work only in object text var range = document.createRange(); var sel = window.getSelection(); range.setStart(el, 1); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); document.getElementById("we").innerHTML = el; // see out put of we id }
<div id="editable" contenteditable="true">dddddddddddddddddddddddddddd <p>dd</p>psss <p>dd</p> <p>dd</p> <p>text text text</p> </div> <p id='we'></p> <button onclick="set_mouse()">focus</button>
Очень сложно установить курсор в правильное положение, когда у вас есть предварительный элемент, например (p) (span) и т. Д. Цель состоит в том, чтобы получить (текст объекта):
<div id="editable" contenteditable="true">dddddddddddddddddddddddddddd<p>dd</p>psss<p>dd</p> <p>dd</p> <p>text text text</p> </div> <p id='we'></p> <button onclick="set_mouse()">focus</button> <script> function set_mouse() { var as = document.getElementById("editable"); el = as.childNodes[1].childNodes[0];//goal is to get ('we') id to write (object Text) because it work only in object text var range = document.createRange(); var sel = window.getSelection(); range.setStart(el, 1); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); document.getElementById("we").innerHTML = el;// see out put of we id } </script>
источник
Я пишу средство выделения синтаксиса (и базовый редактор кода), и мне нужно было знать, как автоматически вводить символ одинарной кавычки и перемещать курсор назад (как многие редакторы кода в настоящее время).
Вот фрагмент моего решения, благодаря большой помощи этой темы, документации MDN и множеству просмотров консоли moz ..
//onKeyPress event if (evt.key === "\"") { let sel = window.getSelection(); let offset = sel.focusOffset; let focus = sel.focusNode; focus.textContent += "\""; //setting div's innerText directly creates new //nodes, which invalidate our selections, so we modify the focusNode directly let range = document.createRange(); range.selectNode(focus); range.setStart(focus, offset); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } //end onKeyPress event
Это в содержательном элементе div
Я оставляю это здесь в качестве благодарности, понимая, что уже есть принятый ответ.
источник
Я сделал это для своего простого текстового редактора.
Отличия от других методов:
Применение
// get current selection const [start, end] = getSelectionOffset(container) // change container html container.innerHTML = newHtml // restore selection setSelectionOffset(container, start, end) // use this instead innerText for get text with keep all spaces const innerText = getInnerText(container) const textBeforeCaret = innerText.substring(0, start) const textAfterCaret = innerText.substring(start)
selection.ts
/** return true if node found */ function searchNode( container: Node, startNode: Node, predicate: (node: Node) => boolean, excludeSibling?: boolean, ): boolean { if (predicate(startNode as Text)) { return true } for (let i = 0, len = startNode.childNodes.length; i < len; i++) { if (searchNode(startNode, startNode.childNodes[i], predicate, true)) { return true } } if (!excludeSibling) { let parentNode = startNode while (parentNode && parentNode !== container) { let nextSibling = parentNode.nextSibling while (nextSibling) { if (searchNode(container, nextSibling, predicate, true)) { return true } nextSibling = nextSibling.nextSibling } parentNode = parentNode.parentNode } } return false } function createRange(container: Node, start: number, end: number): Range { let startNode searchNode(container, container, node => { if (node.nodeType === Node.TEXT_NODE) { const dataLength = (node as Text).data.length if (start <= dataLength) { startNode = node return true } start -= dataLength end -= dataLength return false } }) let endNode if (startNode) { searchNode(container, startNode, node => { if (node.nodeType === Node.TEXT_NODE) { const dataLength = (node as Text).data.length if (end <= dataLength) { endNode = node return true } end -= dataLength return false } }) } const range = document.createRange() if (startNode) { if (start < startNode.data.length) { range.setStart(startNode, start) } else { range.setStartAfter(startNode) } } else { if (start === 0) { range.setStart(container, 0) } else { range.setStartAfter(container) } } if (endNode) { if (end < endNode.data.length) { range.setEnd(endNode, end) } else { range.setEndAfter(endNode) } } else { if (end === 0) { range.setEnd(container, 0) } else { range.setEndAfter(container) } } return range } export function setSelectionOffset(node: Node, start: number, end: number) { const range = createRange(node, start, end) const selection = window.getSelection() selection.removeAllRanges() selection.addRange(range) } function hasChild(container: Node, node: Node): boolean { while (node) { if (node === container) { return true } node = node.parentNode } return false } function getAbsoluteOffset(container: Node, offset: number) { if (container.nodeType === Node.TEXT_NODE) { return offset } let absoluteOffset = 0 for (let i = 0, len = Math.min(container.childNodes.length, offset); i < len; i++) { const childNode = container.childNodes[i] searchNode(childNode, childNode, node => { if (node.nodeType === Node.TEXT_NODE) { absoluteOffset += (node as Text).data.length } return false }) } return absoluteOffset } export function getSelectionOffset(container: Node): [number, number] { let start = 0 let end = 0 const selection = window.getSelection() for (let i = 0, len = selection.rangeCount; i < len; i++) { const range = selection.getRangeAt(i) if (range.intersectsNode(container)) { const startNode = range.startContainer searchNode(container, container, node => { if (startNode === node) { start += getAbsoluteOffset(node, range.startOffset) return true } const dataLength = node.nodeType === Node.TEXT_NODE ? (node as Text).data.length : 0 start += dataLength end += dataLength return false }) const endNode = range.endContainer searchNode(container, startNode, node => { if (endNode === node) { end += getAbsoluteOffset(node, range.endOffset) return true } const dataLength = node.nodeType === Node.TEXT_NODE ? (node as Text).data.length : 0 end += dataLength return false }) break } } return [start, end] } export function getInnerText(container: Node) { const buffer = [] searchNode(container, container, node => { if (node.nodeType === Node.TEXT_NODE) { buffer.push((node as Text).data) } return false }) return buffer.join('') }
источник
Я отредактировал ответ @Liam. Я поместил его в класс со статическими методами, я заставил его функции получать элемент вместо #id и некоторые другие небольшие настройки.
Этот код особенно хорош для фиксации курсора в текстовом поле с форматированным текстом, которое вы можете использовать
<div contenteditable="true">
. Я застрял в этом на несколько дней, прежде чем пришел к приведенному ниже коду.изменить: его ответ и этот ответ содержат ошибку, связанную с нажатием клавиши ввода. Поскольку ввод не считается символом, позиция курсора изменяется после нажатия клавиши ввода. Если я смогу исправить код, я обновлю свой ответ.
edit2: Избавьтесь от головной боли и убедитесь, что
<div contenteditable=true>
это такdisplay: inline-block
. Это исправляет некоторые ошибки, связанные с установкой Chrome<div>
вместо<br>
нажатия клавиши ввода.Как использовать
let richText = document.getElementById('rich-text'); let offset = Cursor.getCurrentCursorPosition(richText); // do stuff to the innerHTML, such as adding/removing <span> tags Cursor.setCurrentCursorPosition(offset, richText); richText.focus();
Код
// Credit to Liam (Stack Overflow) // https://stackoverflow.com/a/41034697/3480193 class Cursor { static getCurrentCursorPosition(parentElement) { var selection = window.getSelection(), charCount = -1, node; if (selection.focusNode) { if (Cursor._isChildOf(selection.focusNode, parentElement)) { node = selection.focusNode; charCount = selection.focusOffset; while (node) { if (node === parentElement) { break; } if (node.previousSibling) { node = node.previousSibling; charCount += node.textContent.length; } else { node = node.parentNode; if (node === null) { break; } } } } } return charCount; } static setCurrentCursorPosition(chars, element) { if (chars >= 0) { var selection = window.getSelection(); let range = Cursor._createRange(element, { count: chars }); if (range) { range.collapse(false); selection.removeAllRanges(); selection.addRange(range); } } } static _createRange(node, chars, range) { if (!range) { range = document.createRange() range.selectNode(node); range.setStart(node, 0); } if (chars.count === 0) { range.setEnd(node, chars.count); } else if (node && chars.count >0) { if (node.nodeType === Node.TEXT_NODE) { if (node.textContent.length < chars.count) { chars.count -= node.textContent.length; } else { range.setEnd(node, chars.count); chars.count = 0; } } else { for (var lp = 0; lp < node.childNodes.length; lp++) { range = Cursor._createRange(node.childNodes[lp], chars, range); if (chars.count === 0) { break; } } } } return range; } static _isChildOf(node, parentElement) { while (node !== null) { if (node === parentElement) { return true; } node = node.parentNode; } return false; } }
источник
Enter
в приведенный выше код? В моем случае это совсем не удобно.Я думаю, что установить курсор в какую-то позицию в contenteditable элементе непросто. Я написал для этого свой собственный код. Он обходит дерево узлов, вычисляя, сколько символов осталось, и устанавливает курсор в нужном элементе. Я не особо много тестировал этот код.
//Set offset in current contenteditable field (for start by default or for with forEnd=true) function setCurSelectionOffset(offset, forEnd = false) { const sel = window.getSelection(); if (sel.rangeCount !== 1 || !document.activeElement) return; const firstRange = sel.getRangeAt(0); if (offset > 0) { bypassChildNodes(document.activeElement, offset); }else{ if (forEnd) firstRange.setEnd(document.activeElement, 0); else firstRange.setStart(document.activeElement, 0); } //Bypass in depth function bypassChildNodes(el, leftOffset) { const childNodes = el.childNodes; for (let i = 0; i < childNodes.length && leftOffset; i++) { const childNode = childNodes[i]; if (childNode.nodeType === 3) { const curLen = childNode.textContent.length; if (curLen >= leftOffset) { if (forEnd) firstRange.setEnd(childNode, leftOffset); else firstRange.setStart(childNode, leftOffset); return 0; }else{ leftOffset -= curLen; } }else if (childNode.nodeType === 1) { leftOffset = bypassChildNodes(childNode, leftOffset); } } return leftOffset; } }
Я также написал код для получения текущей позиции курсора (не тестировал):
//Get offset in current contenteditable field (start offset by default or end offset with calcEnd=true) function getCurSelectionOffset(calcEnd = false) { const sel = window.getSelection(); if (sel.rangeCount !== 1 || !document.activeElement) return 0; const firstRange = sel.getRangeAt(0), startContainer = calcEnd ? firstRange.endContainer : firstRange.startContainer, startOffset = calcEnd ? firstRange.endOffset : firstRange.startOffset; let needStop = false; return bypassChildNodes(document.activeElement); //Bypass in depth function bypassChildNodes(el) { const childNodes = el.childNodes; let ans = 0; if (el === startContainer) { if (startContainer.nodeType === 3) { ans = startOffset; }else if (startContainer.nodeType === 1) { for (let i = 0; i < startOffset; i++) { const childNode = childNodes[i]; ans += childNode.nodeType === 3 ? childNode.textContent.length : childNode.nodeType === 1 ? childNode.innerText.length : 0; } } needStop = true; }else{ for (let i = 0; i < childNodes.length && !needStop; i++) { const childNode = childNodes[i]; ans += bypassChildNodes(childNode); } } return ans; } }
Вы также должны знать, что range.startOffset и range.endOffset содержат смещение символа для текстовых узлов (nodeType === 3) и смещение дочернего узла для узлов элементов (nodeType === 1). range.startContainer и range.endContainer могут ссылаться на любой узел элемента любого уровня в дереве (конечно, они также могут ссылаться на текстовые узлы).
источник
На основе ответа Тима Дауна, но он проверяет последнюю известную «хорошую» текстовую строку. Курсор ставится в самый конец.
Более того, я мог бы также рекурсивно / итеративно проверять последний дочерний элемент каждого последующего последнего дочернего элемента, чтобы найти абсолютный последний «хороший» текстовый узел в DOM.
function onClickHandler() { setCaret(document.getElementById("editable")); } function setCaret(el) { let range = document.createRange(), sel = window.getSelection(), lastKnownIndex = -1; for (let i = 0; i < el.childNodes.length; i++) { if (isTextNodeAndContentNoEmpty(el.childNodes[i])) { lastKnownIndex = i; } } if (lastKnownIndex === -1) { throw new Error('Could not find valid text content'); } let row = el.childNodes[lastKnownIndex], col = row.textContent.length; range.setStart(row, col); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); el.focus(); } function isTextNodeAndContentNoEmpty(node) { return node.nodeType == Node.TEXT_NODE && node.textContent.trim().length > 0 }
<div id="editable" contenteditable="true"> text text text<br>text text text<br>text text text<br> </div> <button id="button" onclick="onClickHandler()">focus</button>
источник