ADA*_*MJR 7 html javascript forms text dom
我有一个充当所见即所得编辑器的 div。它充当文本框,但在其中呈现 Markdown 语法,以显示实时更改。
问题:键入字母时,插入符号位置会重置为 div 的开头。
const editor = document.querySelector('div');
editor.innerHTML = parse('**dlob** *cilati*');
editor.addEventListener('input', () => {
editor.innerHTML = parse(editor.innerText);
});
function parse(text) {
return text
.replace(/\*\*(.*)\*\*/gm, '**<strong>$1</strong>**') // bold
.replace(/\*(.*)\*/gm, '*<em>$1</em>*'); // italic
}Run Code Online (Sandbox Code Playgroud)
div {
height: 100vh;
width: 100vw;
}Run Code Online (Sandbox Code Playgroud)
<div contenteditable />Run Code Online (Sandbox Code Playgroud)
代码笔: https: //codepen.io/ADAMJR/pen/MWvPebK
像 QuillJS 这样的 Markdown 编辑器似乎可以编辑子元素而不编辑父元素。这避免了问题,但我现在确定如何使用此设置重新创建该逻辑。
问题:如何让插入符号位置在打字时不重置?
更新:我已经设法在每个输入上将插入符号位置发送到 div 的末尾。然而,这本质上仍然重置了位置。https://codepen.io/ADAMJR/pen/KKvGNbY
onk*_*kar 15
您需要先获取光标的位置,然后处理和设置内容。然后恢复光标位置。
当存在嵌套元素时,恢复光标位置是一个棘手的部分。而且你每次都在创造新的<strong>元素<em>,旧的元素被丢弃。
const editor = document.querySelector(".editor");
editor.innerHTML = parse(
"For **bold** two stars.\nFor *italic* one star. Some more **bold**."
);
editor.addEventListener("input", () => {
//get current cursor position
const sel = window.getSelection();
const node = sel.focusNode;
const offset = sel.focusOffset;
const pos = getCursorPosition(editor, node, offset, { pos: 0, done: false });
if (offset === 0) pos.pos += 0.5;
editor.innerHTML = parse(editor.innerText);
// restore the position
sel.removeAllRanges();
const range = setCursorPosition(editor, document.createRange(), {
pos: pos.pos,
done: false,
});
range.collapse(true);
sel.addRange(range);
});
function parse(text) {
//use (.*?) lazy quantifiers to match content inside
return (
text
.replace(/\*{2}(.*?)\*{2}/gm, "**<strong>$1</strong>**") // bold
.replace(/(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)/gm, "*<em>$1</em>*") // italic
// handle special characters
.replace(/\n/gm, "<br>")
.replace(/\t/gm, "	")
);
}
// get the cursor position from .editor start
function getCursorPosition(parent, node, offset, stat) {
if (stat.done) return stat;
let currentNode = null;
if (parent.childNodes.length == 0) {
stat.pos += parent.textContent.length;
} else {
for (let i = 0; i < parent.childNodes.length && !stat.done; i++) {
currentNode = parent.childNodes[i];
if (currentNode === node) {
stat.pos += offset;
stat.done = true;
return stat;
} else getCursorPosition(currentNode, node, offset, stat);
}
}
return stat;
}
//find the child node and relative position and set it on range
function setCursorPosition(parent, range, stat) {
if (stat.done) return range;
if (parent.childNodes.length == 0) {
if (parent.textContent.length >= stat.pos) {
range.setStart(parent, stat.pos);
stat.done = true;
} else {
stat.pos = stat.pos - parent.textContent.length;
}
} else {
for (let i = 0; i < parent.childNodes.length && !stat.done; i++) {
currentNode = parent.childNodes[i];
setCursorPosition(currentNode, range, stat);
}
}
return range;
}Run Code Online (Sandbox Code Playgroud)
.editor {
height: 100px;
width: 400px;
border: 1px solid #888;
padding: 0.5rem;
white-space: pre;
}
em, strong{
font-size: 1.3rem;
}Run Code Online (Sandbox Code Playgroud)
<div class="editor" contenteditable ></div>Run Code Online (Sandbox Code Playgroud)
API window.getSelection返回 Node 以及相对于它的位置。每次创建全新元素时,我们都无法使用旧节点对象恢复位置。因此,为了保持简单并拥有更多控制权,我们获取相对于.editorusinggetCursorPosition函数的位置。并且,在设置innerHTML内容后,我们使用 恢复光标位置setCursorPosition。
这两个函数都适用于嵌套元素。
此外,还改进了正则表达式:使用 (.*?) 惰性量词以及向前和向后查找以实现更好的匹配。你可以找到更好的表达方式。
笔记:
getCursorPosition并setCursorPosition保持简单。<br>. 需要white-space: pre在可编辑元素上设置制表符。我尝试在演示中处理\n, 。\t大多数富文本编辑器的做法是保持自己的内部状态,在按键事件上更新它并渲染自定义可视层。例如这样:
\nconst $editor = document.querySelector(\'.editor\');\nconst state = {\n cursorPosition: 0,\n contents: \'hello world\'.split(\'\'),\n isFocused: false,\n};\n\n\nconst $cursor = document.createElement(\'span\');\n$cursor.classList.add(\'cursor\');\n$cursor.innerText = \'\xe1\xa0\x8e\'; // Mongolian vowel separator\n\nconst renderEditor = () => {\n const $contents = state.contents\n .map(char => {\n const $span = document.createElement(\'span\');\n $span.innerText = char;\n return $span;\n });\n \n $contents.splice(state.cursorPosition, 0, $cursor);\n \n $editor.innerHTML = \'\';\n $contents.forEach(el => $editor.append(el));\n}\n\ndocument.addEventListener(\'click\', (ev) => {\n if (ev.target === $editor) {\n $editor.classList.add(\'focus\');\n state.isFocused = true;\n } else {\n $editor.classList.remove(\'focus\');\n state.isFocused = false;\n }\n});\n\ndocument.addEventListener(\'keydown\', (ev) => {\n if (!state.isFocused) return;\n \n switch(ev.key) {\n case \'ArrowRight\':\n state.cursorPosition = Math.min(\n state.contents.length, \n state.cursorPosition + 1\n );\n renderEditor();\n return;\n case \'ArrowLeft\':\n state.cursorPosition = Math.max(\n 0, \n state.cursorPosition - 1\n );\n renderEditor();\n return;\n case \'Backspace\':\n if (state.cursorPosition === 0) return;\n delete state.contents[state.cursorPosition-1];\n state.contents = state.contents.filter(Boolean);\n state.cursorPosition = Math.max(\n 0, \n state.cursorPosition - 1\n );\n renderEditor();\n return;\n default:\n // This is very naive\n if (ev.key.length > 1) return;\n state.contents.splice(state.cursorPosition, 0, ev.key);\n state.cursorPosition += 1;\n renderEditor();\n return;\n } \n});\n\nrenderEditor();Run Code Online (Sandbox Code Playgroud)\r\n.editor {\n position: relative;\n min-height: 100px;\n max-height: max-content;\n width: 100%;\n border: black 1px solid;\n}\n\n.editor.focus {\n border-color: blue;\n}\n\n.editor.focus .cursor {\n position: absolute;\n border: black solid 1px;\n border-top: 0;\n border-bottom: 0;\n animation-name: blink;\n animation-duration: 1s;\n animation-iteration-count: infinite;\n}\n\n@keyframes blink {\n from {opacity: 0;}\n 50% {opacity: 1;}\n to {opacity: 0;}\n}Run Code Online (Sandbox Code Playgroud)\r\n<div class="editor"></div>Run Code Online (Sandbox Code Playgroud)\r\n