console
let font_metrics = document.querySelector('.font-metrics');
let text = document.querySelector('.text');
let lines = text.textContent.split('\n');
let cursor = document.querySelector('.cursor');
let markers = document.querySelector('.markers');
let lineHeight = 20;
let font_size = 13;
let anchor = {
row: 0,
col: 0
}
let pointer = {
row: 0,
col: 0
}
let selectStart = false;
let anchorColumn = 0;
function debounce(callback, time) {
return function () {
clearTimeout(callback.timer);
callback.timer = setTimeout(callback, time);
}
}
let restartBlink = debounce(function () {
cursor.classList.add('cursor-blink');
}, 800);
function measureText(fontMetrics, text) {
if (!text) {
return 0;
}
fontMetrics.textContent = text;
return fontMetrics.getBoundingClientRect().width;
}
function cursorTranslate(x, y) {
cursor.style.left = x + 'px';
cursor.style.top = y + 'px';
}
function onCursorActive() {
let line = lines[pointer.row];
let left = measureText(font_metrics, line.slice(0, pointer.col));
let top = pointer.row * lineHeight;
cursorTranslate(left, top);
cursor.classList.remove('cursor-blink');
restartBlink();
}
function onSelect() {
updateSelection();
}
function updateSelection() {
let rows = Math.abs(pointer.row - anchor.row) + 1;
let _isBackwards = isBackwards();
let isSameRow = anchor.row == pointer.row;
clearSelection();
if (_isBackwards) {
for (let i = pointer.row; i <= anchor.row; ++i) {
let marker = document.createElement('div');
let line = lines[i];
let isStartRow = i == pointer.row;
let isEndRow = i == anchor.row;
let isSameRow = pointer.row == anchor.row;
let left = 0;
let width = 0;
let top = i * lineHeight;
let eof = false;
if (isSameRow) {
left = measureText(font_metrics, line.slice(0, pointer.col));
width = measureText(font_metrics, line.slice(pointer.col, anchor.col));
eof = anchor.col === line.length;
} else if (isStartRow) {
left = measureText(font_metrics, line.slice(0, pointer.col));
width = measureText(font_metrics, line.slice(pointer.col));
eof = true;
} else if (isEndRow) {
left = 0;
width = measureText(font_metrics, line.slice(0, anchor.col));
eof = false;
} else {
left = 0;
width = measureText(font_metrics, line);
eof = true;
}
if (eof) {
width += 5;
}
marker.style.position = 'absolute';
marker.style.left = left + 'px';
marker.style.top = top + 'px';
marker.style.width = width + 'px';
marker.style.height = lineHeight + 'px';
marker.style.background = '#ddd';
if (!markers.contains(marker)) {
markers.appendChild(marker);
}
}
} else {
for (let i = anchor.row; i < rows + anchor.row; ++i) {
let marker = document.createElement('div');
let line = lines[i];
let isEndRow = i == pointer.row;
let isStartRow = i == anchor.row;
let left = 0;
let width = 0;
let top = i * lineHeight;
let eof = false;
if (isSameRow) {
left = measureText(font_metrics, line.slice(0, anchor.col));
width = measureText(font_metrics, line.slice(anchor.col, pointer.col));
eof = line.length && pointer.col == line.length;
} else if (isStartRow) {
left = measureText(font_metrics, line.slice(0, anchor.col));
width = measureText(font_metrics, line.slice(anchor.col));
eof = true;
} else if (isEndRow) {
left = 0;
width = measureText(font_metrics, line.slice(0, pointer.col));
eof = false;
} else {
left = 0;
width = measureText(font_metrics, line);
eof = true;
}
if (eof) {
width += 5;
}
marker.style.position = 'absolute';
marker.style.left = left + 'px';
marker.style.top = top + 'px';
marker.style.width = width + 'px';
marker.style.height = lineHeight + 'px';
marker.style.background = '#ddd';
if (!markers.contains(marker)) {
markers.appendChild(marker);
}
}
}
}
function clearSelection() {
markers.innerHTML = "";
}
function isBackwards() {
return pointer.row < anchor.row || (pointer.row == anchor.row && pointer.col < anchor.col);
}
function moveTo(row, col) {
let lastRow = pointer.row;
let lastCol = pointer.col;
if (row >= lines.length) {
row = Math.max(0, lines.length - 1);
col = pointer.col = lines[pointer.row].length;
} else if (row < 0) {
row = 0;
col = 0;
} else {
pointer.row = row;
pointer.col = Math.min(lines[row].length, Math.max(0, col));
}
if (lastRow != pointer.row || lastCol != pointer.col) {
onCursorActive();
}
}
function moveCursorRight() {
let line = lines[pointer.row];
if (pointer.col >= line.length) {
moveTo(pointer.row + 1, 0);
} else {
moveTo(pointer.row, pointer.col + 1);
}
anchorColumn = pointer.col;
}
function moveCursorLeft() {
let line = lines[pointer.row - 1];
if (pointer.col == 0) {
moveTo(pointer.row - 1, line ? line.length : 0);
} else {
moveTo(pointer.row, pointer.col - 1);
}
anchorColumn = pointer.col;
}
function moveCursorUp() {
moveTo(pointer.row - 1, anchorColumn);
}
function moveCursorDown() {
moveTo(pointer.row + 1, anchorColumn);
}
function findCoordsFromPoints(x, y) {
let row = Math.floor(y / lineHeight);
let col = 0;
let line = lines[row] || lines[lines.length - 1] || [];
let start = Math.floor(x / font_size);
let left = 0
for (; start <= line.length; ++start) {
left = measureText(font_metrics, line.slice(0, start));
if (left >= x) {
break;
}
}
col = start;
return {
col, row
}
}
function fromPoints(x, y) {
let pos = findCoordsFromPoints(x, y);
moveTo(pos.row, pos.col);
anchorColumn = pos.col;
}
function setSelectStart() {
selectStart = true;
setTimeout(() => {
anchor.row = pointer.row;
anchor.col = pointer.col;
}, 0)
}
function isPointInRange(x, y) {
let pos = findCoordsFromPoints(x, y);
let row = pos.row;
let col = pos.col;
if (isBackwards()) {
} else {
return (row >= anchor.row && row <= pointer.row);
}
}
document.onkeydown = function (e) {
let k = e.key;
if (k == 'ArrowRight') {
moveCursorRight();
} else if (k == 'ArrowLeft') {
moveCursorLeft();
} else if (k == 'ArrowDown') {
moveCursorDown();
} else if (k == 'ArrowUp') {
moveCursorUp();
}
}
text.onmousedown = function (e) {
if (e.which == 1) {
let textBound = text.getBoundingClientRect();
let x = e.clientX - textBound.left;
let y = e.clientY - textBound.top;
fromPoints(x, y);
setSelectStart();
}
}
text.oncontextmenu = function (e) {
e.preventDefault();
}
document.onmouseup = function () {
selectStart = false;
}
document.onmousemove = function (e) {
if (selectStart) {
let textBound = text.getBoundingClientRect();
let x = e.clientX - textBound.left;
let y = e.clientY - textBound.top;
fromPoints(x, y);
onSelect();
}
}
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=, initial-scale=">
<meta http-equiv="X-UA-Compatible" content="">
<title></title>
</head>
<body>
<div class="line">
<div class="markers"></div>
<pre class="text">var oop = require("./lib/oop");
var EventEmitter = require("./lib/event_emitter").EventEmitter;
var Anchor = exports.Anchor = function (doc, row, column) {
this.$onChange = this.onChange.bind(this);
this.attach(doc);
if (typeof column == "undefined")
this.setPosition(row.row, row.column);
else
this.setPosition(row, column);
};
(function () {
oop.implement(this, EventEmitter);
this.isEmpty = function () {
return this.$isEmpty || (
this.anchor.row == this.lead.row &&
this.anchor.column == this.lead.column
);
};
this.isMultiLine = function () {
return !this.$isEmpty && this.anchor.row != this.cursor.row;
};
this.getCursor = function () {
return this.lead.getPosition();
};
this.setSelectionAnchor = function (row, column) {
this.$isEmpty = false;
this.anchor.setPosition(row, column);
};
}).call(Anchor.prototype);</pre>
<div class="cursor cursor-blink"></div>
<div class="font-metrics"></div>
</div>
</body>
</html>
* {
margin: 0;
padding: 0;
}
@keyframes blink {
0% {
opacity: 0;
}
49% {
opacity: 0;
}
50% {
opacity: 1;
}
99% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.line .text {
position: relative;
font-size: 13px;
font-family: Consolas;
user-select: none;
cursor: text;
line-height: 20px;
font-weight: normal;
z-index: 1;
background: transparent;
}
.line {
position: relative;
height: auto;
background: #fff;
}
.line .cursor {
position: absolute;
left: 0;
top: 0;
height: 20px;
width: 0;
border-right: 1px solid #000;
cursor: text;
z-index: 2;
}
.line .cursor.cursor-blink {
animation: blink 800ms infinite;
}
.line .font-metrics {
position: absolute;
left: 0;
top: 0;
width: auto;
height: auto;
padding: 0;
margin: 0;
border: none;
font-size: 13px;
font-family: Consolas;
font-weight: normal;
z-index: -1;
white-space: pre;
}
.line .markers {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 0;
}