Показать курсор в пользовательском textarea без отображения его текста

у меня есть пользовательский textarea. В этом примере он делает буквы красными или зелеными, случайным образом.

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>

есть выходной div с текстовым полем над ним. Таким образом, textarea не скрывает никаких красочных вещей под ним, его цвет и фон установлены на прозрачный. Здесь все работает, за исключением того, что курсор (мигающий курсор, предоставляемый агентом пользователя) прозрачен.

есть ли способ, чтобы показать карет, не делая текст textarea видимым?

если я сделаю div над textarea вместо этого и дам его pointer-events: none, текстовая область все еще видна внизу. Эти договоренности также затрудняют плавную прокрутку, поэтому это не работает для меня.

2 ответов


просто вставьте свой собственный каретку!

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>

Я здесь <span> #caret вставлен в div, который мигает каждые 500 мс, переключая его скрытый атрибут с помощью JS. Чтобы реплицировать поведение браузера, я должен был определить, является ли это selectionStart или selectionEnd который каретка была на самом деле, и сделать его оставаться твердым во время ввода текста.

это немного сложнее достичь, когда пролеты не являются фиксированными длина или вложены, но это проще, чем возиться с contentEditable с более сложным маркером. Эта функция вставит курсор в нужное место:

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);
        }
    }
}

Использование (где i позиция, чтобы вставить его):

var caret = document.createElement('span');
caret.id = 'caret';
caret.appendChild(document.createTextNode('\xA0'));
insertNodeAtPosition(caret, mydiv, i);
clearTimeout(blinkTimeout);
blinkTimeout = setTimeout(blink, 500);

почему бы просто не использовать <div contenteditable="true"></div> вместо <textarea></textarea>?. С этим вам не нужно extra textarea. Вижу демо здесь.

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);

кроме того, чтобы переместить курсор в конце, когда вы поместите новый текст внутри div я использовал эту функцию из ответ:

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();
    }
}