diff --git a/src/editor/caret.js b/src/editor/caret.js index f93e9604d5..c56022d8c6 100644 --- a/src/editor/caret.js +++ b/src/editor/caret.js @@ -15,50 +15,104 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {needsCaretNodeBefore, needsCaretNodeAfter} from "./render"; + export function setCaretPosition(editor, model, caretPosition) { const sel = document.getSelection(); sel.removeAllRanges(); const range = document.createRange(); + const {offset, lineIndex, nodeIndex} = getLineAndNodePosition(model, caretPosition); + const lineNode = editor.childNodes[lineIndex]; + + let focusNode; + // empty line with just a <br> + if (nodeIndex === -1) { + focusNode = lineNode; + } else { + focusNode = lineNode.childNodes[nodeIndex]; + // make sure we have a text node + if (focusNode.nodeType === Node.ELEMENT_NODE && focusNode.firstChild) { + focusNode = focusNode.firstChild; + } + } + range.setStart(focusNode, offset); + range.collapse(true); + sel.addRange(range); +} + +function getLineAndNodePosition(model, caretPosition) { const {parts} = model; - const {index} = caretPosition; + const partIndex = caretPosition.index; + const lineResult = findNodeInLineForPart(parts, partIndex); + const {lineIndex} = lineResult; + let {nodeIndex} = lineResult; let {offset} = caretPosition; + // we're at an empty line between a newline part + // and another newline part or end/start of parts. + // set offset to 0 so it gets set to the <br> inside the line container + if (nodeIndex === -1) { + offset = 0; + } else { + // move caret out of uneditable part (into caret node, or empty line br) if needed + ({nodeIndex, offset} = moveOutOfUneditablePart(parts, partIndex, nodeIndex, offset)); + } + return {lineIndex, nodeIndex, offset}; +} + +function findNodeInLineForPart(parts, partIndex) { let lineIndex = 0; let nodeIndex = -1; - for (let i = 0; i <= index; ++i) { + + let prevPart = null; + // go through to parts up till (and including) the index + // to find newline parts + for (let i = 0; i <= partIndex; ++i) { const part = parts[i]; - if (part && part.type === "newline") { - if (i < index) { - lineIndex += 1; - nodeIndex = -1; - } else { - // if index points at a newline part, - // put the caret at the end of the previous part - // so it stays on the same line - const prevPart = parts[i - 1]; - offset = prevPart ? prevPart.text.length : 0; + if (part.type === "newline") { + lineIndex += 1; + nodeIndex = -1; + prevPart = null; + } else { + nodeIndex += 1; + if (needsCaretNodeBefore(part, prevPart)) { + nodeIndex += 1; + } + // only jump over caret node if we're not at our destination node already, + // as we'll assume in moveOutOfUneditablePart that nodeIndex + // refers to the node corresponding to the part, + // and not an adjacent caret node + if (i < partIndex) { + const nextPart = parts[i + 1]; + const isLastOfLine = !nextPart || nextPart.type === "newline"; + if (needsCaretNodeAfter(part, isLastOfLine)) { + nodeIndex += 1; + } + } + prevPart = part; + } + } + + return {lineIndex, nodeIndex}; +} + +function moveOutOfUneditablePart(parts, partIndex, nodeIndex, offset) { + // move caret before or after uneditable part + const part = parts[partIndex]; + if (part && !part.canEdit) { + if (offset === 0) { + nodeIndex -= 1; + const prevPart = parts[partIndex - 1]; + // if the previous node is a caret node, it's empty + // so the offset can stay at 0 + // only when it's not, we need to set the offset + // at the end of the node + if (!needsCaretNodeBefore(part, prevPart)) { + offset = prevPart.text.length; } } else { nodeIndex += 1; + offset = 0; } } - let focusNode; - const lineNode = editor.childNodes[lineIndex]; - if (lineNode) { - focusNode = lineNode.childNodes[nodeIndex]; - if (!focusNode) { - focusNode = lineNode; - } else if (focusNode.nodeType === Node.ELEMENT_NODE) { - focusNode = focusNode.childNodes[0]; - } - } - // node not found, set caret at end - if (!focusNode) { - range.selectNodeContents(editor); - range.collapse(false); - } else { - // make sure we have a text node - range.setStart(focusNode, offset); - range.collapse(true); - } - sel.addRange(range); + return {nodeIndex, offset}; }