Skip to content

Instantly share code, notes, and snippets.

@mzechmeister
Last active October 4, 2021 22:37
Show Gist options
  • Save mzechmeister/3bdffdb10386ad9f8e3be2dcf84fb3a3 to your computer and use it in GitHub Desktop.
Save mzechmeister/3bdffdb10386ad9f8e3be2dcf84fb3a3 to your computer and use it in GitHub Desktop.
focus nested editable elements
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<style>
li {
display: block;
padding: 0 2px;
}
li.hover {
background: lightblue;
}
li:not(.show) {
display: none;
}
span {
padding: 0 0px;
background-color: #DDF;
}
span:not(:focus) + ul { /* the next sibiling, but ul is previous sibling*/
display: none;
}
</style>
<body>
<div id="edit" contentEditable style="width:400px; border: 1px solid">
wwwwww++++++ press $ to insert here a span</div>
<span id="suggest_div" contentEditable=False style="position: relative;"><ul id="suggest" style="position: absolute; z-index:99; top: 9px; left: 1px; padding: 0; width: 60.333px; background: white; border: 1px solid #ccc">
<li style="color:blue">Edge</li>
<li style="color:red">Firefox</li>
<li style="color:purple">Firefo</li>
<li style="color:green">Chrome</li>
<li style="color:orange">Opera</li>
<li style="color:cyan">Safari</li>
</ul></span>
<script>
handle_hover = function(e){
if (e.key=="ArrowDown" || e.key=="ArrowUp") {
active = document.querySelector('.show.hover')
// find next proposal
if (active) {
while (active = active[(e.key=="ArrowUp"? 'previous':'next') + 'ElementSibling']) {
if (active.classList.contains('show')) break
}
if (!active) { // stick to the element (first/last if not found)
e.preventDefault()
return
}
}
old = document.querySelector('.hover')
old && old.classList.remove('hover')
active && active.classList.add('hover')
e.preventDefault()
}
if (!e.key) {
// mouseover
old = document.querySelector('.hover')
old && old.classList.remove('hover')
e.target.classList.add('hover')
}
if (e.key == "Enter") {
apply_hit(active)
e.preventDefault()
}
}
function apply_hit(e) {
active = e.target || e
if (active) {
uu.innerHTML = '$' + active.innerHTML
uu.style.color = active.style.color
setEndOfContenteditable(uu);
}
//suggest.style.display = "none"
e.preventDefault && e.preventDefault(); // do not insert newline
return // prevent caret movement
}
focusToParent = function(e){
console.log('blur', e);
//suggest.style.display = "none"
document.getElementById("edit").focus()
return false
}
suggest.onmouseover = handle_hover
suggest.onmousedown = apply_hit
handle_suggestentries = function(e) {
if (!["Enter", "ArrowLeft", "ArrowRight"].includes(e.key))
suggest.style.display = ""
for (x of suggest.children) {
if (x.innerHTML.toLowerCase().match(uu.innerHTML.toLowerCase().replace("$","")))
x.classList.add('show')
else x.classList.remove('show', 'hover')
}
// propose first element
document.querySelector('.show.hover') ||
(active = document.querySelector('.show')) && active.classList.add('hover')
}
edit.onkeydown = function(e){
if (e.key == "$") {
// open a span
uu = document.createElement('span')
uu.tabIndex = -1
uu.onkeydown = handle_hover
uu.onkeyup = handle_suggestentries
uu.onblur = focusToParent
insertNodeAtCursor(uu)
uu.before(suggest_div)
}
}
document.getElementById("edit").onkeyup = function(e){
// focus to child
sel = window.getSelection();
foc = sel.focusNode.parentElement
console.log('up', foc, e.target,document.getElementById("edit"))
if (foc.tagName == "SPAN" && e.target==document.getElementById("edit")) {
foc.focus()
}
}
function insertNodeAtCursor(node) {
let selection = window.getSelection()
let range = selection.getRangeAt(0);
range.deleteContents()
range.insertNode(node)
setEndOfContenteditable(uu)
//uu.focus()
}
function setEndOfContenteditable(contentEditableElement) {
var range = document.createRange(); //Create a range (a range is a like the selection but invisible)
range.selectNodeContents(contentEditableElement); //Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
var selection = window.getSelection();//get the selection object (allows you to change selection)
selection.removeAllRanges();//remove any selections already made
selection.addRange(range);//make the range you have just created the visible selection
}
</script>
<p onclick="browser.focus()"><strong>Note:</strong> The datalist tag is not supported in Safari 12.0 (or earlier).</p>
</body>
</html>
@mzechmeister
Copy link
Author

mzechmeister commented Oct 3, 2021

https://gistpreview.github.io/?78fb1f2af80115a6b59f813407892186

Revision 2:
Simplified a lot. It can be handled with focus alone (no caret manipulation).

Almost perfect. Only when moving to the right border of the nested span, there appears a small flash of the parent border.

Another small issue is that a char insert before the nested div removes the space. But it does not occur with nested spans.

Revision 3: always mark a proposal (default: first entry)
https://gistpreview.github.io/?3bdffdb10386ad9f8e3be2dcf84fb3a3

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