Skip to content

Instantly share code, notes, and snippets.

@mzechmeister
Last active January 3, 2022 21:00
Show Gist options
  • Save mzechmeister/d0333d6e46746efc2f06e61547d69047 to your computer and use it in GitHub Desktop.
Save mzechmeister/d0333d6e46746efc2f06e61547d69047 to your computer and use it in GitHub Desktop.
pseudo editable field (code mirror lite)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<style>
#cursor {
display: inline;
}
#cursor.collapsed {
animation: blink 1s;
outline: 1px solid red;
animation-iteration-count: infinite;
}
span {
color: green;
padding: 5px;
}
span:before {
content: "5";
vertical-align: super;
font-size: smaller;
}
@keyframes blink {
50% { outline-color: #ffffff00; }
}
button {
font-size: large;
font-weight: bold;
}
</style>
</head>
<body>
<textarea id="ta" style="height:150px">$The top element is a textarea and the div at the bottom a pseudo editable field.$ Sometime mouse selection does not work.$</textarea>
<button onclick="ta.focus(); document.execCommand('undo', false, null)">↶</button>
<button onclick="ta.focus(); document.execCommand('redo', false, null)">↷</button>
<div id="cursor"></div>
<div id="mydiv" contenteditable></div>
<script>
cursor = document.getElementById('cursor')
function parse(t) {
return t.replace(/\$/g, '<span>$</span>')
}
function pseudoCursor() {
collapsed = ta.selectionStart == ta.selectionEnd
pointA = getPoint(ta.selectionStart)
cursor.classList.toggle('collapsed', collapsed)
if (collapsed) {
// indicate the ta position in editable div
let range = document.createRange()
range.setStart(...pointA)
range.insertNode(cursor)
mydiv.normalize() // triggers again selection change, if new cursor after old cursor
} else if (!block) {
// hand over the ta selection to the editable div
point = getPoint(ta.selectionEnd)
if (ta.selectionDirection == "backward") sel.setBaseAndExtent(...point, ...pointA)
else sel.setBaseAndExtent(...pointA, ...point)
}
}
function getPos(node, offset) {
let range = document.createRange()
range.setStart(mydiv, 0)
range.setEnd(node, offset)
pos = range.toString().length
return pos
}
function getPoint(offset) {
mydiv.after(cursor) // avoid miscounting with inline cursor, normalisation can be skipped
for (node of [...mydiv.childNodes]) {
len = (node.innerText || node).length
if (offset <= len) break
offset -= len
}
return [node, offset]
}
sel = window.getSelection()
block = false
document.addEventListener("selectionchange", function(e) {
cursor.classList.remove("collapsed")
if (document.activeElement == ta) pseudoCursor()
prev_sel = [getPos(sel.anchorNode, sel.anchorOffset), getPos(sel.focusNode, sel.focusOffset)]
if (!block && document.activeElement == mydiv && prev_sel[1] == prev_sel[0]) {
ta.setSelectionRange(...prev_sel)
}
})
// Firefox ~68 + enable dom.select_events.textcontrols.enabled
ta.addEventListener("selectionchange", function(e) {
pseudoCursor()
e.stopPropagation()
})
ta.oninput = e => {
mydiv.innerHTML = parse(ta.value)
if (e.inputType == 'deleteContentForward')
pseudoCursor()
// other keys call pseudoCursor via selectionchange
}
mydiv.ondrop = function(e) {
edata = null
if (document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(e.clientX, e.clientY)
droppos = getPos(range.startContainer, range.startOffset)
edata = e.dataTransfer.getData('Text')
} else
droppos = getPos(e.rangeParent, e.rangeOffset)
}
mydiv.onpaste = function (e) {
// at least for edge, not firefox
edata = (e.clipboardData || window.clipboardData).getData('Text')
}
mydiv.onmouseup = function (e) {
sel.isCollapsed && ta.focus()
}
mydiv.oninput = function(e) {
console.log(e.inputType)
block = true
ta.focus()
if (prev_sel[0] > prev_sel[1])
prev_sel = prev_sel.reverse()
if ("deleteByCut deleteByDrag deleteContentBackward deleteContentForward".includes(e.inputType)) {
ta.setSelectionRange(...prev_sel)
document.execCommand('delete')
} else {
data = e.data || (e.dataTransfer && e.dataTransfer.getData('Text')) || edata
edata = null
if ("insertFromPaste insertText".includes(e.inputType)) {
ta.setSelectionRange(...prev_sel)
} else if (e.inputType == "insertFromDrop") {
if (droppos > prev_sel[0]) droppos -= prev_sel[1] -prev_sel[0]
ta.setSelectionRange(droppos, droppos)
droppos = null
}
document.execCommand('insertText', false, data)
}
block = false
}
mydiv.innerHTML = parse(ta.value)
</script>
</body>
</html>
@mzechmeister
Copy link
Author

To inspect codemirror search for the textarea element and inspect the attached events.

https://github.com/codemirror/CodeMirror/blob/c1941628cdda92c32977b3a26b0a0b70c9a8f9b5/src/input/TextareaInput.js#L224

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment