6

I have a custom textarea. In this example, it makes the letters red or green, randomly.

var mydiv = document.getElementById('mydiv'),
    myta = document.getElementById('myta');
function updateDiv() {
  var fc;
  while (fc = mydiv.firstChild) mydiv.removeChild(fc);
  for (var i = 0; i < myta.value.length; i++) {
    var span = document.createElement('span');
    span.className = Math.random() < 0.5 ? 'green' : 'red';
    span.appendChild(document.createTextNode(myta.value[i]));
    mydiv.appendChild(span);
  }
};
myta.addEventListener('input', updateDiv);
body { position: relative }
div, textarea {
  -webkit-text-size-adjust: none;
  width: 100%;
  white-space: pre-wrap;
  word-wrap: break-word;
  overflow-wrap: break-word;
  font: 1rem sans-serif;
  padding: 2px;
  margin: 0;
  border-radius: 0;
  border: 1px solid #000;
  resize: none;
}
textarea {
  position: absolute;
  top: 0;
  color: transparent;
  background: transparent;
}
.red { color: #f00 }
.green { color: #0f0 }
<div id="mydiv"></div>
<textarea id="myta" autofocus=""></textarea>

There's an output div with a textarea over it. So the textarea doesn't cover up any of the colorful things below it, its color and background are set to transparent. Everything works here, except that the caret (the flashing cursor provided by the user agent) is transparent.

Is there a way to show the caret without making the textarea's text visible?

If I make the div above the textarea instead and give it pointer-events: none, the textarea is still visible underneath. This arrangements also makes smooth scrolling difficult, so it doesn't work for me.

bjb568
  • 11,089
  • 11
  • 50
  • 71

2 Answers2

3

Just insert your own caret!

function blink() {
  document.getElementById('caret').hidden ^= 1;
  blinkTimeout = setTimeout(blink, 500);
}
var mydiv = document.getElementById('mydiv'),
    myta = document.getElementById('myta'),
    blinkTimeout = setTimeout(blink, 500),
    lastSelectionStart = 0,
    lastSelectionEnd = 0,
    whichSelection = true;
function updateDiv() {
  var fc;
  while (fc = mydiv.firstChild) mydiv.removeChild(fc);
  if (myta.selectionStart != lastSelectionStart) {
    lastSelectionStart = myta.selectionStart;
    whichSelection = false;
  }
  if (myta.selectionEnd != lastSelectionEnd) {
    lastSelectionEnd = myta.selectionEnd;
    whichSelection = true;
  }
  var cursorPos = whichSelection ? myta.selectionEnd : myta.selectionStart;
  for (var i = 0; i < myta.value.length; i++) {
    if (i == cursorPos) {
      var caret = document.createElement('span');
      caret.id = 'caret';
      caret.appendChild(document.createTextNode('\xA0'));
      mydiv.appendChild(caret);
      clearTimeout(blinkTimeout);
      blinkTimeout = setTimeout(blink, 500);
    }
    var span = document.createElement('span');
    span.className = Math.random() < 0.5 ? 'green' : 'red';
    span.appendChild(document.createTextNode(myta.value[i]));
    mydiv.appendChild(span);
  }
  if (myta.value.length == cursorPos) {
    var caret = document.createElement('span');
    caret.id = 'caret';
    caret.appendChild(document.createTextNode('\xA0'));
    mydiv.appendChild(caret);
    clearTimeout(blinkTimeout);
    blinkTimeout = setTimeout(blink, 500);
  }
};
myta.addEventListener('input', updateDiv);
myta.addEventListener('focus', updateDiv);
myta.addEventListener('mousedown', function() {
  setTimeout(updateDiv, 0);
});
myta.addEventListener('keydown', function() {
  setTimeout(updateDiv, 0);
});
myta.addEventListener('blur', function() {
  document.getElementById('caret').hidden = true;
  clearTimeout(blinkTimeout);
});
body { position: relative }
div, textarea {
  -webkit-text-size-adjust: none;
  width: 100%;
  white-space: pre-wrap;
  word-wrap: break-word;
  overflow-wrap: break-word;
  font: 1rem sans-serif;
  padding: 2px;
  margin: 0;
  border-radius: 0;
  border: 1px solid #000;
  resize: none;
}
textarea {
  position: absolute;
  top: 0;
  color: transparent;
  background: transparent;
}
.red { color: #f00 }
.green { color: #0f0 }
#caret {
  display: inline-block;
  position: absolute;
  width: 1px;
  background: #000;
}
#caret[hidden] { display: none }
<div id="mydiv"><span id="caret">&nbsp;</span></div>
<textarea id="myta" autofocus=""></textarea>

I have here a <span> #caret inserted into the div which blinks every 500ms by toggling its hidden attribute using JS. To replicate browser behavior, I had to detect whether it was the selectionStart or the selectionEnd which the caret was actually at, and make it remain solid while text was being input.

This is a bit harder to achieve when the spans aren't of fixed length or are nested, but it's easier than fiddling with contentEditable with a more complex highlighter. This function will insert the caret in the right spot:

function insertNodeAtPosition(node, refNode, pos) {
    if (typeof(refNode.nodeValue) == 'string') refNode.parentNode.insertBefore(node, refNode.splitText(pos));
    else {
        for (var i = 0; i < refNode.childNodes.length; i++) {
            var chNode = refNode.childNodes[i];
            if (chNode.textContent.length <= pos && i != refNode.childNodes.length - 1) pos -= chNode.textContent.length;
            else return insertNodeAtPosition(node, chNode, pos);
        }
    }
}

Usage (where i is the position to insert it):

var caret = document.createElement('span');
caret.id = 'caret';
caret.appendChild(document.createTextNode('\xA0'));
insertNodeAtPosition(caret, mydiv, i);
clearTimeout(blinkTimeout);
blinkTimeout = setTimeout(blink, 500);
bjb568
  • 11,089
  • 11
  • 50
  • 71
1

Why not simply use a <div contenteditable="true"></div> instead <textarea></textarea>?. With this you don't need the extra textarea. See a demo here.

HTML:

<div id="myta" autofocus="" contenteditable="true"></div>

JavaScript:

var myta = document.getElementById('myta');
function updateDiv() {
    var fc;
    var text = myta.innerText || myta.textContent;
    while (fc = myta.firstChild) myta.removeChild(fc);
    for (var i = 0; i < text.length; i++) {
        var span = document.createElement('span');
        span.className = Math.random() < 0.5 ? 'green' : 'red';
        span.appendChild(document.createTextNode(text[i]));
        myta.appendChild(span);
    }
    placeCaretAtEnd(myta);
};
myta.addEventListener('input', updateDiv);

Also, to move the caret at the end when you put the new text inside the div I used that function from this answer:

function placeCaretAtEnd(el) {
    el.focus();
    if (typeof window.getSelection != "undefined"
        && typeof document.createRange != "undefined") {
        var range = document.createRange();
        range.selectNodeContents(el);
        range.collapse(false);
        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    } else if (typeof document.body.createTextRange != "undefined") {
        var textRange = document.body.createTextRange();
        textRange.moveToElementText(el);
        textRange.collapse(false);
        textRange.select();
    }
}
Community
  • 1
  • 1
lmgonzalves
  • 6,518
  • 3
  • 22
  • 41
  • contenteditable isn't a form element and can lead to data loss since its content isn't saved by the browser. It's also pretty messy to work with. – bjb568 Jun 22 '15 at 04:18
  • Related: http://stackoverflow.com/questions/20728150/unusual-shape-of-a-textarea#comment31094671_20728150 and http://stackoverflow.com/questions/20728150/unusual-shape-of-a-textarea#comment31158616_20728275 – bjb568 Jun 22 '15 at 04:24
  • If you want the `textarea` too, see [this new demo](https://jsfiddle.net/bb5jmw95/1/). I only add `myta.value = text;` to set text in the `textarea` too. Then you can simply hide the `textarea` with `display: none;`. Hope this help :) – lmgonzalves Jun 22 '15 at 04:28
  • Hmm, this might work with some tweaking of event responding behavior (i.e. preventDefault()), I'll play around with it tomorrow. – bjb568 Jun 22 '15 at 04:36