我有一個充當所見即所得編輯器的 div。它充當文字框,但在其中呈現 Markdown 語法,以顯示即時變更。
問題:鍵入字母時,插入符號位置會重設為 div 的開頭。
const editor = document.querySelector('div'); editor.innerHTML = parse('**dlob** *cilati*'); editor.addEventLis tener('input', () => { editor.innerHTML = parse(editor.innerText); }); function parse(text) { return text .replace(/**(.*)**/gm, '**<strong></strong>**') // bold .replace(/*(.*)*/gm, '*<em></em>*'); // italic }
div { height: 100vh; width: 100vw; }
<div contenteditable />
Codepen:https://codepen.io/ADAMJR/pen/MWvPebK
像 QuillJS 這樣的 Markdown 編輯器似乎可以編輯子元素而不編輯父元素。這避免了問題,但我現在確定如何使用此設定重新建立該邏輯。
問題:如何讓插入符號位置在打字時不重置?
更新: 我已經設法在每個輸入上將插入符號位置發送到 div 的末尾。然而,這本質上仍然重置了位置。 https://codepen.io/ADAMJR/pen/KKvGNbY
P粉3930309172023-11-09 18:34:46
大多數富文本編輯器的做法是保持自己的內部狀態,在按鍵事件上更新它並渲染自訂視覺層。例如這樣:
const $editor = document.querySelector('.editor'); const state = { cursorPosition: 0, contents: 'hello world'.split(''), isFocused: false, }; const $cursor = document.createElement('span'); $cursor.classList.add('cursor'); $cursor.innerText = ''; // Mongolian vowel separator const renderEditor = () => { const $contents = state.contents .map(char => { const $span = document.createElement('span'); $span.innerText = char; return $span; }); $contents.splice(state.cursorPosition, 0, $cursor); $editor.innerHTML = ''; $contents.forEach(el => $editor.append(el)); } document.addEventListener('click', (ev) => { if (ev.target === $editor) { $editor.classList.add('focus'); state.isFocused = true; } else { $editor.classList.remove('focus'); state.isFocused = false; } }); document.addEventListener('keydown', (ev) => { if (!state.isFocused) return; switch(ev.key) { case 'ArrowRight': state.cursorPosition = Math.min( state.contents.length, state.cursorPosition + 1 ); renderEditor(); return; case 'ArrowLeft': state.cursorPosition = Math.max( 0, state.cursorPosition - 1 ); renderEditor(); return; case 'Backspace': if (state.cursorPosition === 0) return; delete state.contents[state.cursorPosition-1]; state.contents = state.contents.filter(Boolean); state.cursorPosition = Math.max( 0, state.cursorPosition - 1 ); renderEditor(); return; default: // This is very naive if (ev.key.length > 1) return; state.contents.splice(state.cursorPosition, 0, ev.key); state.cursorPosition += 1; renderEditor(); return; } }); renderEditor();
.editor { position: relative; min-height: 100px; max-height: max-content; width: 100%; border: black 1px solid; } .editor.focus { border-color: blue; } .editor.focus .cursor { position: absolute; border: black solid 1px; border-top: 0; border-bottom: 0; animation-name: blink; animation-duration: 1s; animation-iteration-count: infinite; } @keyframes blink { from {opacity: 0;} 50% {opacity: 1;} to {opacity: 0;} }
P粉0601123962023-11-09 14:42:18
需要先取得遊標的位置,然後對內容進行處理和設定。然後恢復遊標位置。
當存在嵌套元素時,恢復遊標位置是一個棘手的部分。此外,您每次都會建立新的 和
元素,舊的元素將被丟棄。
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, "****") // bold .replace(/(?*") // italic // handle special characters .replace(/\n/gm, "
") .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; }
.editor { height: 100px; width: 400px; border: 1px solid #888; padding: 0.5rem; white-space: pre; } em, strong{ font-size: 1.3rem; }