JSFiddle - React, Tailwind, and code Playground

by Josh Pullen

HTML

<div class="editorWrapper">
  <div class="editor"></div>
  <div class="cursor"></div>
</div>

CSS

.editorWrapper {
  position:relative;
}
.editor {
  min-width: 100px;
  min-height: 100px;
  background: #ddd;
  font-family: monospace;
  white-space: nowrap;
  overflow-x: auto;
  cursor: text;
}
.cursor {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 15px; /* Line height (should really be set by JS) */
  background: red;
}

JavaScript

var editorWrapper = document.getElementsByClassName('editorWrapper')[0];
var editor = document.getElementsByClassName('editor')[0];
var cursor = document.getElementsByClassName('cursor')[0];

var phantomInput = document.createElement('textarea');
document.body.append(phantomInput);

var hasFocus = false;
const charWidth = 7;
const lineHeight = 15;

editorWrapper.addEventListener('click', function(e) {
	let x = e.clientX - this.offsetLeft;
  let y = e.clientY - this.offsetTop;
  let col = Math.round(x / charWidth); // x in terms of characters
  let row = Math.floor(y / lineHeight); // y in terms of lines
  let lines = phantomInput.value.split('\n');
  if(row > lines.length - 1) {
  	row = lines.length - 1;
    col = lines[lines.length - 1].length;
  } else if(col > lines[row].length) {
    col = lines[row].length;
  }
  cursor.style.left = col * charWidth + 'px';
  cursor.style.top = row * lineHeight + 'px';
	phantomInput.focus();
  phantomInput.selectionStart = phantomInput.selectionEnd = coordsToLoc(col, row);
});

phantomInput.addEventListener('focus', function() {
	hasFocus = true;
  editor.style.background = 'yellow';
  cursor.style.display = 'block';
});

phantomInput.addEventListener('blur', function() {
	hasFocus = false;
  editor.style.background = '#ddd';
  cursor.style.display = 'none';
});

function locToCoords(loc) {
	// Convert numeric location in text to row/col coordinates
  let lines = phantomInput.value.split('\n');
  let row = 0;
  let cumulativeLoc = 0;
  while(cumulativeLoc + lines[row].length + 1 < loc) {
  	cumulativeLoc += lines[row].length + 1;
    row++;
  }
  let col = loc - cumulativeLoc;
  return {
    'col': col,
    'row': row
  };
}
function coordsToLoc(col, row) {
	let lines = phantomInput.value.split('\n');
  let loc = 0;
  for(let i = 0; i < row; i++) {
  	loc += lines[i].length + 1;
  }
  loc += col;
  return loc;
}

function inputChange() {
  editor.innerText = phantomInput.value;
  
  let cursorCoords =...