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>
<style>
#cursor {
display: inline;
}
#cursor:not(.collapsed) {
background-color: #acf;
}
#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; }
}
</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>
<div id="cursor" ></div>
<div id="mydiv" onclick="let pos = getSelectionPosition(event); ta.setSelectionRange(...pos.sort()); ta.focus()"></div>
<script>
cursor = document.getElementById('cursor')
function parse(t) {
return t.replace(/\$/g, '<span>X</span>')
}
ta.oninput = e => {
mydiv.innerHTML = parse(ta.value)
}
function pseudoCursor(nodeA, posA, node,pos) {
let range = document.createRange()
range.setStart(nodeA, posA)
range.setEnd(node, pos)
cursor.classList.toggle('collapsed', range.collapsed);
range.insertNode(cursor)
}
function getPos(node, offset) {
pos = offset
while (node = node.previousSibling)
pos += (node.nodeType == Node.TEXT_NODE ? node : node.innerText).length
return pos
}
function getSelectionPosition(e) {
if (document.caretRangeFromPoint) {
// edge, chrome, android
range = document.caretRangeFromPoint(e.clientX, e.clientY)
nodeA = range.startContainer
posA = range.startOffset
var node = range.endContainer
var pos = range.endOffset
} else {
// firefox
var node = e.rangeParent
var pos = e.rangeOffset
}
posA = getPos(nodeA, posA)
pos = getPos(node, pos)
return [posA, pos]
}
function posi(offset) {
for (node of [...mydiv.childNodes]) {
len = (node.nodeType == Node.TEXT_NODE ? node : node.innerText).length
if (offset > len) {
offset -= len
} else break
}
return [node, offset]
}
function update() {
// map ta selection to div selection
selA = ta.selectionStart
selB = ta.selectionEnd
pseudoCursor(...posi(selA), ...posi(selB))
}
var mouse = false
mydiv.onmousedown = function(e) {
mouse = true
if (!document.caretRangeFromPoint) {
// firefox, remember start selection
nodeA = e.rangeParent
posA = e.rangeOffset
}
}
mydiv.onmouseup = function(e) {
mouse = false
}
document.addEventListener("selectionchange", function(e) {
!mouse && update()
})
// Firefox ~68 + enable dom.select_events.textcontrols.enabled
ta.addEventListener("selectionchange", function(e) {
update()
e.stopPropagation()
})
mydiv.innerHTML = parse(ta.value)
</script>
</body>
</html>
@mzechmeister
Copy link
Author

mzechmeister commented Nov 14, 2021

preview latest: https://gistpreview.github.io/?d0333d6e46746efc2f06e61547d69047

  • Rev 41 mouseup to focus ta (should be better than Rev 39)
  • Rev 40 Cleaned.
  • Rev 39 fix: text selection with mouse difficult (focus went to ta).
  • Rev 38 feat: Undo/redo button.
  • Rev 37 Cleaned.
  • Rev 36: fix: edge requires clipboard for dataTransfer.
  • Rev 35: fix: Handling of backward selection.
  • Rev 34 refact: Using e.inputType to detect various events.
  • Rev 33 fix: oninput fires again after drop.
  • Rev 32 chore: New variable name insText.
  • Rev 31 chore: New variable names cutA and cut.
  • Rev 30 fix: No pseudocursor for del key.
  • Rev 29 chore: divcutsel renamed to cutpos.
  • Rev 28 Better handling of drop (selection history).
  • Rev 27 Better handling of drop (selection history).
  • Rev 26 Fix drop not working properly anymore.
  • Rev 25 Space formatting.
  • Rev 24 Handle input in editable div.
  • Rev 23 Simplifications; update function removed.
  • Rev 22 Handback textarea selection to div selection.
  • Rev 21 A try to handle drag and drop. Simplier but now textarea and div must have the same content. Undo with ctrl-Z ends up with selection all.
  • Rev 20 Redirect drop from mydiv to textarea.
  • Rev 19 Cleaning + simplification.
  • Rev 18 getClientRects.
  • Rev 17 fix minor caret misbehaviour (jump at pos 0 -> 1, hide caret when focus out).
  • Rev 16 sel.anchorNode returns body not ta in chrome and edge, instead use document.activeElement.
  • Rev 15 range.toString().length instead of node loop.
  • Rev 14: div is contenteditable, but only to simplify position detection.
  • Rev 13: refact: Cursor as relative div. But: textnode dissociates, extended range not marked anymore.
  • Rev 12: fix: caret behaviour at nested span (located at middle start or end).
  • Rev 11: refact: overlay cursor the absolute.
  • Rev 10: last version with inline span for caret;
    fix backward selection (desktop only), double click selection not working
  • Rev 9: fix mouse selection does not work.
  • Rev 8: replaceAll not working on some android, replaced by replace + / /g.
  • Rev 7: mousedown+preventDefault to keep focus on textarea, and rangeParent instead of selection. Now, build-in text selection on mobiles does not popup. There seems to be no redundant selectionchange calls.
  • Rev 6: refact: onselectionchange instead of key events. Now pseudo caret is also moved for spacebar swiping on mobiles. onclick does not set the caret (firefox).
  • Rev 5: feat: Text selection with keyboard.
  • Rev 4: As before, click sets the caret properly (curpos computed with node loop; would allow to corrected of string length changes by parser)
  • Rev 3: Fixed: Click sets now the caret properly. However, text selection are now deleted. Since the parse changes the string length at dollar, this needs to be taken into account.
  • Rev 0 (https://gist.github.com/mzechmeister/d0333d6e46746efc2f06e61547d69047/e4732a0a72b6bea5923081af66e7134dc0ca211a)

Problems of contenteditable:

  • mixes content and layout, we want to edit to content only, the rendering should be done by parsing.
  • edit history tricky, needs obsolete document.execCommand and document.execCommand + insertHTML does not work.
  • different browser behaviour for caret position

@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