console
let box = document.querySelector('.box');
let text = box.querySelector('.text');
let lines = text.innerText.split('\n');
let font_metrics = box.querySelector('.font-metrics');
let markers = box.querySelector('.marker');
let textInput = box.querySelector('.text-input');
let gutter = document.querySelector('.gutter');
let gutterInnerY = gutter.querySelector('.innerY');
let gutterLayer = gutter.querySelector('.layer');
let container = document.querySelector('.container');
let lh = 20;
let fz = 13;
let renderCount = Math.floor(box.clientHeight / lh) + 2;
let renderStart = 0;
let innerX = box.querySelector('.innerX');
let innerY = box.querySelector('.innerY');
let cursor = box.querySelector('.cursor');
let anchor = {
row: 0,
col: 0
}
let lead = {
row: 0,
col: 0
}
let lastCol = 0;
let startSelect = false;
let ranges = [];
let oldSnippets = [];
let inMultiSelectMode = false;
let willClearSelection = false;
let cursorBlinkId = null;
let offsetXInBox = 0;
let offsetYInBox = 0;
let longest = {
line: '',
width: 0,
row: 0
}
let keys = {
control: false,
shift: false,
alt: false
}
let brackets = {
'{': '}',
'[': ']',
'(': ')',
"'": "'",
'"': '"'
}
function Range(startRow, startCol, endRow, endCol) {
this.start = {
row: startRow,
col: startCol
}
this.end = {
row: endRow,
col: endCol
}
}
Range.prototype.setStart = function (row, col) {
this.start = {
row,
col
}
}
Range.prototype.setEnd = function (row, col) {
this.end = {
row,
col
}
}
let fontMetrics = {
charSizes: {},
measure(char) {
let charWidth = parseFloat(this.charSizes[char]);
if (Number.isNaN(charWidth)) {
font_metrics.textContent = char;
this.charSizes[char] = charWidth = font_metrics.getBoundingClientRect().width;
}
return charWidth;
}
}
function measureText(text) {
if (!text) {
return 0;
}
let width = 0;
for (let s of text) {
width += fontMetrics.measure(s);
}
return width;
}
function debounce(callback, time) {
return function () {
clearTimeout(callback.timeouId);
callback.timeouId = setTimeout(callback, time);
}
}
function throttle(callback, time) {
return function () {
if (callback.wait) {
return;
}
callback();
callback.wait = true;
setTimeout(() => {
callback();
callback.wait = false;
}, time);
}
}
function clipChildren(el, start, end) {
if (start > end) {
return;
}
let children = [];
for (let i = 0; i < el.childElementCount; ++i) {
let child = el.children[i];
if (!child) {
continue;
}
if (i < start || i >= end) {
children.push(child);
}
}
children.forEach(child => {
child.remove();
});
return children;
}
let recoverBlink = debounce(function () {
startCursorBlink();
}, 600);
function startCursorBlink() {
let isVisible = getComputedStyle(cursor).display == 'block';
if (isVisible) {
cursor.style.display = 'none'
} else {
cursor.style.display = 'block';
}
cursorBlinkId = setTimeout(() => {
startCursorBlink();
}, 600);
}
function stopCursorBlink() {
cursor.style.display = 'block';
clearTimeout(cursorBlinkId);
}
function stopBlink() {
stopCursorBlink();
recoverBlink();
}
function scrollIfNeeded() {
let scrollLines = Math.floor(box.scrollTop / lh);
let baseRenderCount = Math.floor(box.clientHeight / lh);
let lineHead = lines[lead.row].slice(0, lead.col);
let offsetX = measureText(lineHead);
let scrollLeft = box.scrollLeft;
if (lead.row < scrollLines) {
box.scroll(box.scrollLeft, lead.row * lh);
} else if (lead.row > scrollLines + baseRenderCount) {
box.scroll(box.scrollLeft, (lead.row - (scrollLines + baseRenderCount)) * lh + box.scrollTop);
}
if (offsetX < scrollLeft) {
box.scroll(offsetX, box.scrollTop);
} else if (offsetX > box.scrollLeft + box.clientWidth) {
let sl = (offsetX - (box.scrollLeft + box.clientWidth)) + box.scrollLeft + 2;
box.scroll(sl, box.scrollTop);
}
}
function onCursorActive() {
stopBlink();
scrollIfNeeded();
}
function moveCursorTo(row, col) {
if (!isCollapsed()) {
clearSelection();
}
lastCol = col;
let pos = clipPosToDocument({ row, col });
anchor.row = lead.row = pos.row;
anchor.col = lead.col = pos.col;
updateCursorPos();
onCursorActive();
}
function moveCursorLeft() {
if (!isCollapsed()) {
if (isBackwards(anchor, lead)) {
moveCursorTo(lead.row, lead.col);
} else {
moveCursorTo(anchor.row, anchor.col);
}
return;
}
if (lead.col <= 0) {
let prevLine = lines[lead.row - 1];
let col = prevLine ? prevLine.length : 0;
moveCursorTo(lead.row - 1, col);
} else {
moveCursorTo(lead.row, lead.col - 1);
}
}
function moveCursorRight() {
if (!isCollapsed()) {
if (isBackwards(anchor, lead)) {
moveCursorTo(anchor.row, anchor.col);
} else {
moveCursorTo(lead.row, lead.col);
}
return;
}
let line = lines[lead.row];
if (lead.col >= line.length) {
moveCursorTo(lead.row + 1, 0);
} else {
moveCursorTo(lead.row, lead.col + 1);
}
}
function moveCursorDown() {
moveCursorTo(lead.row + 1, lastCol);
}
function moveCursorUp() {
moveCursorTo(lead.row - 1, lastCol);
}
function moveCursorLineStart() {
moveCursorTo(lead.row, 0);
}
function moveCursorLineEnd() {
moveCursorTo(lead.row, lines[lead.row].length);
}
function selectTo(row, col) {
lastCol = col;
let pos = clipPosToDocument({ row, col });
lead.row = pos.row;
lead.col = pos.col;
updateSelection();
updateCursorPos();
onCursorActive();
}
function selectLeft() {
if (lead.col == 0) {
let prevLine = lines[lead.row - 1];
let col = prevLine ? prevLine.length : 0;
selectTo(lead.row - 1, col);
} else {
selectTo(lead.row, lead.col - 1);
}
}
function selectRight() {
let line = lines[lead.row];
if (lead.col == line.length) {
selectTo(lead.row + 1, 0);
} else {
selectTo(lead.row, lead.col + 1);
}
}
function select(start, end) {
let _anchor = clipPosToDocument({ row: start.row, col: start.col });
let _lead = clipPosToDocument({ row: end.row, col: end.col });
anchor.row = _anchor.row;
anchor.col = _anchor.col;
selectTo(_lead.row, _lead.col);
}
function selectAll() {
let maxLineNo = lines.length - 1;
let lastLine = lines[maxLineNo];
select({ row: 0, col: 0 }, { row: maxLineNo, col: lastLine.length });
}
function selectRow(row) {
let pos = clipPosToDocument({ row, col: 0 });
let line = lines[pos.row];
select({ row: pos.row, col: 0 }, { row: pos.row, col: line.length });
}
function insertText(text) {
if (!isCollapsed()) {
backspace();
}
_insertText(text);
if (text in brackets) {
moveCursorRight();
} else {
moveCursorTo(lead.row, lead.col + text.length);
}
renderVisibleLines();
if (lines[longest.row] != longest.line) {
updateScrollWidth();
}
}
function _insertText(text) {
let line = lines[lead.row];
lines[lead.row] = line.slice(0, lead.col) + text + (text in brackets ? brackets[text] : '') + line.slice(lead.col);
}
function insertLines(inputLines) {
if (!isCollapsed()) {
backspace();
}
inputLines.forEach((line, index) => {
let curRow = lead.row;
let curLine = lines[curRow];
let curLineHead = curLine.slice(0, lead.col);
let curLineTail = curLine.slice(lead.col);
lines[curRow] = curLineHead + line + curLineTail;
anchor.col = lead.col = lead.col + line.length;
if (index < inputLines.length - 1) {
curLine = lines[lead.row];
lines[curRow] = curLine.slice(0, lead.col);
lines.splice(curRow + 1, 0, curLine.slice(lead.col));
let pos = clipPosToDocument({ row: curRow + 1, col: 0 });
anchor.col = lead.col = pos.col;
anchor.row = lead.row = pos.row;
}
});
updateLineNumber();
updateScrollHeight();
updateScrollWidth();
renderVisibleLines();
moveCursorTo(lead.row, lead.col);
}
function backspace() {
let rows = Math.abs(anchor.row - lead.row);
if (isCollapsed()) {
let line = lines[lead.row];
let row = lead.row;
if (lead.col == 0 && lead.row > 0) {
moveCursorLeft();
let deleteLine = lines.splice(row, 1);
lines[lead.row] += deleteLine;
renderVisibleLines();
rows = 1;
} else if (lead.col > 0) {
lines[lead.row] = line.slice(0, lead.col - 1) + line.slice(lead.col);
moveCursorLeft();
renderVisibleLines();
}
} else {
let backwards = isBackwards(anchor, lead);
let isSame = lead.row == anchor.row;
let startRow = backwards ? lead.row : anchor.row;
let startCol = backwards ? lead.col : anchor.col;
let endRow = backwards ? anchor.row : lead.row;
let endCol = backwards ? anchor.col : lead.col;
if (isSame) {
let line = lines[lead.row];
lines[lead.row] = line.slice(0, startCol) + line.slice(endCol);
moveCursorLeft();
renderVisibleLines();
clearSelection();
} else {
let startLine = lines[startRow];
let endLine = lines[endRow];
let deleteCount = endRow - startRow;
lines.splice(startRow + 1, deleteCount);
lines[startRow] = startLine.slice(0, startCol) + endLine.slice(endCol);
moveCursorLeft();
renderVisibleLines();
clearSelection();
}
}
updateScrollHeight();
box.scrollBy(0, -rows * lh);
if (rows) {
updateLineNumber();
}
if (lines[longest.row] != longest.line) {
updateScrollWidth();
}
}
function breakLine() {
let row = lead.row;
let line = lines[row];
if (typeof line == 'string') {
if (!isCollapsed()) {
backspace();
breakLine();
} else {
lines[row] = line.slice(0, lead.col);
lines.splice(row + 1, 0, line.slice(lead.col));
moveCursorTo(row + 1, 0);
renderVisibleLines();
}
}
updateScrollHeight();
updateLineNumber();
}
function _breakLine() {
let row = lead.row;
let line = lines[row];
lines[row] = line.slice(0, lead.col);
lines.splice(row + 1, 0, line.slice(lead.col));
}
function onPaste(e) {
e.preventDefault();
if (e instanceof ClipboardEvent) {
let text = e.clipboardData.getData('text/plain');
let _lines = text.split('\n');
if (_lines.length == 1) {
insertText(_lines[0]);
} else {
insertLines(_lines);
}
}
}
function cutSelection() {
if (isCollapsed()) {
return;
}
copySelectionText();
backspace();
}
function copySelectionText() {
navigator.clipboard.writeText(getSelectionText());
}
function renderVisibleLines() {
let start = renderStart;
let end = start + renderCount;
let snippets = lines.slice(start, end);
let code = snippets.join('\n');
text.innerText = code;
text.style.top = start * lh + 'px';
oldSnippets = snippets;
}
function layout() {
let scrollLines = Math.floor(box.scrollTop / lh);
let maxLineNo = (renderCount + scrollLines).toString();
let width = measureText(maxLineNo);
let gutterPadding = 20;
gutter.style.width = width + gutterPadding + 'px';
box.style.left = width + gutterPadding + 'px';
box.style.width = `calc(100% - ${width + gutterPadding}px)`;
}
function updateLineNumber() {
let start = Math.floor(box.scrollTop / lh);
let end = Math.min(lines.length - start, renderCount);
let fragment = document.createDocumentFragment();
for (let i = 0; i < end; ++i) {
let elt = gutterLayer.children[i] || document.createElement('div');
elt.innerHTML = start + i + 1;
elt.classList.add('item');
elt.style.top = (i + start) * lh + 'px';
if (!gutterLayer.contains(elt)) {
fragment.appendChild(elt);
}
}
if (fragment.childElementCount) {
gutterLayer.appendChild(fragment);
}
clipChildren(gutterLayer, 0, end);
layout();
}
function updateScrollHeight() {
innerY.style.height = lines.length * lh + 'px';
gutterInnerY.style.height = lines.length * lh + 'px';
}
function updateScrollWidth() {
longest.width = 0;
longest.line = '';
longest.row = 0;
lines.forEach((line, row) => {
let width = measureText(line);
if (width >= longest.width) {
longest.width = width;
longest.line = line;
longest.row = row;
}
});
innerX.style.width = longest.width + 'px';
}
function updateCursorPos() {
let lineHead = lines[lead.row].slice(0, lead.col);
cursor.style.top = textInput.style.top = lead.row * lh + 'px';
cursor.style.left = textInput.style.left = measureText(lineHead) + 'px';
}
function showCursor() {
cursor.style.display = 'block';
}
function hideCursor() {
cursor.style.display = 'none';
}
function drawRangeMarker(range) {
let backwards = isBackwards(range.start, range.end);
let startRow = backwards ? range.end.row : range.start.row;
let endRow = backwards ? range.start.row : range.end.row;
let startCol = backwards ? range.end.col : range.start.col;
let endCol = backwards ? range.start.col : range.end.col;
let start = Math.max(startRow, Math.floor(box.scrollTop / lh));
let end = Math.min(endRow, start + renderCount);
let fragment = document.createDocumentFragment();
let linesPos = [];
for (let i = start; i <= end; ++i) {
let line = lines[i];
let isStart = i == startRow;
let isEnd = i == endRow;
let isSame = startRow == endRow;
let lineSnipptes = '';
let lineHead = '';
if (isSame) {
lineSnipptes = line.slice(startCol, endCol);
lineHead = line.slice(0, startCol);
} else if (isStart) {
lineSnipptes = line.slice(startCol, line.length);
lineHead = line.slice(0, startCol);
} else if (isEnd) {
lineSnipptes = line.slice(0, endCol);
lineHead = '';
} else {
lineSnipptes = line;
lineHead = '';
}
let width = measureText(lineSnipptes);
let left = measureText(lineHead);
let top = i * lh;
let elt = markers.children[i - start] || document.createElement('div');
let eof = isEnd ? false : true;
if (eof) {
width += 8;
}
linesPos.push({
start: left,
end: left + width,
elt,
line
});
elt.classList.add('marker-item');
elt.setAttribute('row', i);
elt.style.width = width + 'px';
elt.style.height = lh + 'px';
elt.style.left = left + 'px';
elt.style.top = top + 'px';
if (!markers.contains(elt)) {
fragment.appendChild(elt);
}
}
linesPos.forEach((item, index) => {
let prev = linesPos[index - 1];
let next = linesPos[index + 1];
let es = item.elt.style;
if ((!prev && item.start > 0) || (prev && prev.end < item.start)) {
es.borderTopLeftRadius = '3px';
} else {
es.borderTopLeftRadius = '';
}
if (!prev || prev.end < item.end || prev.start > item.end) {
es.borderTopRightRadius = '3px';
} else {
es.borderTopRightRadius = '';
}
if ((!next && item.start > 0) || (next && next.end < item.start)) {
es.borderBottomLeftRadius = '3px';
} else {
es.borderBottomLeftRadius = '';
}
if (!next || next.end < item.end || next.start > item.end) {
es.borderBottomRightRadius = '3px';
} else {
es.borderBottomRightRadius = '';
}
let rl_bg, rl, rr_bg, rr;
let children = item.elt.children;
if (children[0]) {
rl_bg = children[0];
} else {
item.elt.appendChild((rl_bg = document.createElement('div')));
}
rl_bg.classList.add('rl', 'bg', 'radius');
if (children[1]) {
rl = children[1];
} else {
item.elt.appendChild((rl = document.createElement('div')));
}
rl.classList.add('rl', 'radius');
if (children[2]) {
rr_bg = children[2];
} else {
item.elt.appendChild((rr_bg = document.createElement('div')));
}
rr_bg.classList.add('rr', 'bg', 'radius');
if (children[3]) {
rr = children[3];
} else {
item.elt.appendChild((rr = document.createElement('div')));
}
rr.classList.add('rr', 'radius');
if (prev && prev.start < item.end && prev.end > item.end) {
rr.style.borderTopLeftRadius = '3px';
} else {
rr.style.borderTopLeftRadius = '';
}
if (next && next.start < item.end && next.end > item.end) {
rr.style.borderBottomLeftRadius = '3px';
} else {
rr.style.borderBottomLeftRadius = '';
}
if (next && next.start < item.start && next.end > item.start) {
rl.style.borderBottomRightRadius = '3px';
} else {
rl.style.borderBottomRightRadius = '';
}
});
if (fragment.childElementCount) {
markers.appendChild(fragment);
}
clipChildren(markers, 0, end - start + 1);
}
function updateSelectionMarker() {
for (let i = 0; i < ranges.length; ++i) {
let range = ranges[i];
drawRangeMarker(range);
}
}
function updateSelection() {
if (inMultiSelectMode) {
} else {
let first = ranges[0];
if (!first) {
first = new Range(anchor.row, anchor.col, lead.row, lead.col);
ranges.push(first);
} else {
first.setStart(anchor.row, anchor.col);
first.setEnd(lead.row, lead.col);
}
}
updateSelectionMarker();
}
function clearSelection() {
ranges = [];
markers.innerHTML = "";
willClearSelection = false;
anchor.row = lead.row;
anchor.col = lead.col;
}
function getRangeText(range) {
let backwards = isBackwards(range.start, range.end);
let startRow = backwards ? range.end.row : range.start.row;
let endRow = backwards ? range.start.row : range.end.row;
let startCol = backwards ? range.end.col : range.start.col;
let endCol = backwards ? range.start.col : range.end.col;
let _lines = [];
for (let i = startRow; i <= endRow; ++i) {
let isStart = i == startRow;
let isEnd = i == endRow;
let isSame = startRow == endRow;
let line = lines[i];
if (isSame) {
_lines.push(line.slice(startCol, endCol));
} else if (isStart) {
_lines.push(line.slice(startCol));
} else if (isEnd) {
_lines.push(line.slice(0, endCol));
} else {
_lines.push(line);
}
}
return _lines.join('\n');
}
function getSelectionText() {
let texts = [];
ranges.forEach(range => {
texts.push(getRangeText(range));
});
return texts.join('\n');
}
function isBackwards(anchor, lead) {
return lead.row < anchor.row || (lead.row == anchor.row && lead.col < anchor.col);
}
function isPointInRange(x, y, range) {
let backwards = isBackwards(range.start, range.end);
let startRow = backwards ? range.end.row : range.start.row;
let endRow = backwards ? range.start.row : range.end.row;
let startCol = backwards ? range.end.col : range.start.col;
let endCol = backwards ? range.start.col : range.end.col;
let row = findPosFromPoints(x, y).row;
if (row < startRow || row > endRow) {
return false;
}
let line = lines[row];
if (startRow == endRow) {
let lineHead = line.slice(0, startCol);
let lineSnipptes = line.slice(0, endCol);
let min = measureText(lineHead);
let max = measureText(lineSnipptes);
if (x < min || x > max) {
return false;
}
} else if (row == startRow) {
let lineHead = line.slice(0, startCol);
let lineWidth = measureText(line);
let markerOffset = measureText(lineHead);
if (x < markerOffset || x > lineWidth) {
return false;
}
} else if (row == endRow) {
let lineHead = line.slice(0, endCol);
let markerWidth = measureText(lineHead);
if (x < 0 || x > markerWidth) {
return false;
}
} else if (x < 0 || x > measureText(line)) {
return false;
}
return true;
}
function findPosFromPoints(x, y) {
let st = box.scrollTop;
let sl = box.scrollLeft;
let sx = sl + x;
let sy = st + y;
let startCol = 0;
let startRow = Math.min(lines.length - 1, Math.max(Math.floor(sy / lh), 0));
let line = lines[startRow];
for (startCol = 0; startCol < line.length; ++startCol) {
let head = line.slice(0, startCol);
let lastCh = head[head.length - 1] || '';
let charEnd = measureText(head);
let charStart = measureText(head) - measureText(lastCh);
if (sx >= charStart && sx <= charEnd) {
let sub = charEnd - charStart;
let sub2 = sx - charStart;
if (!Math.round(sub2 / sub)) {
--startCol;
}
break;
}
}
return {
col: startCol,
row: startRow
}
}
function clipPosToDocument(pos) {
let p = {}
if (pos.row > lines.length - 1) {
p.row = lines.length - 1;
p.col = lines[p.row].length;
} else if (pos.row < 0) {
p.row = 0;
p.col = 0;
} else {
p.row = pos.row;
p.col = Math.min(lines[pos.row].length, Math.max(0, pos.col));
}
return p;
}
function clipPosToContainer(clientX, clientY) {
let rect = box.getBoundingClientRect();
let x = Math.min(rect.left + box.clientWidth, Math.max(0, clientX - rect.left));
let y = Math.min(rect.top + box.clientHeight, Math.max(0, clientY - rect.top));
return { x, y };
}
function isCollapsed() {
return anchor.row == lead.row && anchor.col == lead.col;
}
function isPointInWorkspace(clientX, clientY) {
let rect = box.getBoundingClientRect();
if (clientX < rect.left || clientX > (box.clientWidth + rect.left)) {
return false;
}
if (clientY < rect.top || clientY > (box.clientHeight + rect.top)) {
return false;
}
return true;
}
function startScrollDown(speed) {
if ((typeof speed == "number" && speed < 1) || !speed) {
speed = 1;
} else if (speed > 3) {
speed = 3;
}
startScrollDown.speed = speed;
if (!startScrollDown.timerId) {
stopScrollDown();
if (Math.ceil(box.scrollTop + box.clientHeight) > box.scrollHeight) {
return;
}
startScrollDown.timerId = setInterval(() => {
let remainder = box.scrollTop % lh;
if (remainder) {
box.scroll(box.scrollLeft, box.scrollTop - remainder + lh);
} else {
box.scrollBy(0, startScrollDown.speed * lh);
}
if (Math.ceil(box.scrollTop + box.clientHeight) > box.scrollHeight) {
stopScrollDown();
}
}, 20);
}
}
function stopScrollDown() {
clearInterval(startScrollDown.timerId);
startScrollDown.timerId = null;
}
function startScrollUp(speed) {
if ((typeof speed == "number" && speed < 1) || !speed) {
speed = 1;
} else if (speed > 3) {
speed = 3;
}
startScrollUp.speed = speed;
if (!startScrollUp.timerId) {
stopScrollUp();
if (box.scrollTop == 0) {
return;
}
startScrollUp.timerId = setInterval(() => {
let remainder = box.scrollTop % lh;
if (remainder) {
box.scroll(box.scrollLeft, box.scrollTop - remainder);
} else {
box.scrollBy(0, -startScrollUp.speed * lh);
}
if (box.scrollTop == 0) {
stopScrollUp();
}
}, 20);
}
}
function stopScrollUp() {
clearInterval(startScrollUp.timerId);
startScrollUp.timerId = null;
}
function startScrollRight() {
if (!startScrollRight.timerId) {
stopScrollRight();
let lineWidth = measureText(lines[lead.row]);
if (box.scrollLeft + box.clientWidth >= lineWidth) {
return;
}
startScrollRight.timerId = setInterval(() => {
box.scrollBy(20, 0);
lineWidth = measureText(lines[lead.row]);
if (box.scrollLeft + box.clientWidth >= lineWidth + 20) {
stopScrollRight();
}
}, 20);
}
}
function stopScrollRight() {
clearInterval(startScrollRight.timerId);
startScrollRight.timerId = null;
}
function startScrollLeft() {
if (!startScrollLeft.timerId) {
stopScrollLeft();
if (box.scrollLeft == 0) {
return;
}
startScrollLeft.timerId = setInterval(() => {
box.scrollBy(-20, 0);
if (box.scrollLeft == 0) {
stopScrollLeft();
}
}, 20);
}
}
function stopScrollLeft() {
clearInterval(startScrollLeft.timerId);
startScrollLeft.timerId = null;
}
function stopAutoScroll() {
stopScrollDown();
stopScrollUp();
stopScrollLeft();
stopScrollRight();
}
container.onmousedown = function () {
setTimeout(() => {
textInput.focus({ preventScroll: true });
}, 0);
}
box.onmousedown = function (e) {
if (!isPointInWorkspace(e.clientX, e.clientY)) {
return;
}
let posClipped = clipPosToContainer(e.clientX, e.clientY);
let inRange = isPointInRange(posClipped.x, posClipped.y, { start: anchor, end: lead });
let pos = findPosFromPoints(posClipped.x, posClipped.y);
if (inRange && !isCollapsed()) {
willClearSelection = true;
startSelect = false;
box.draggable = true;
} else {
startSelect = true;
box.draggable = false;
clearSelection();
moveCursorTo(pos.row, pos.col);
}
}
box.onscroll = function () {
let start = Math.floor(box.scrollTop / lh);
if (start != renderStart) {
renderStart = start;
renderVisibleLines();
}
if (startSelect) {
let pos = findPosFromPoints(offsetXInBox, offsetYInBox);
selectTo(pos.row, pos.col);
} else {
updateSelection();
updateCursorPos();
}
updateLineNumber();
gutter.scroll(0, box.scrollTop);
}
box.ondragover = function (e) {
e.preventDefault();
}
box.ondragstart = function (e) {
if (e instanceof DragEvent) {
e.dataTransfer.setDragImage(new Image(), 0, 0);
e.dataTransfer.setData('text/plain', getSelectionText());
}
}
box.oncontextmenu = function (e) {
e.preventDefault();
}
box.onwheel = function (e) {
e.preventDefault();
if (e.deltaY < 0) {
box.scrollBy(0, -70);
} else {
box.scrollBy(0, 70);
}
}
textInput.onkeydown = function (e) {
let k = e.key.toLowerCase();
if (k in keys) {
keys[k] = true;
}
if (keys.shift && k == 'arrowleft') {
selectLeft();
} else if (keys.shift && k == 'arrowright') {
selectRight();
} else if (keys.control && k == 'a') {
selectAll();
} else if (keys.control && k == 'c') {
copySelectionText();
} else if (keys.control && k == 'x') {
cutSelection();
} else if (k == 'arrowleft') {
moveCursorLeft();
} else if (k == 'arrowright') {
moveCursorRight();
} else if (k == 'arrowdown') {
moveCursorDown();
} else if (k == 'arrowup') {
moveCursorUp();
} else if (k == 'end') {
moveCursorLineEnd();
} else if (k == 'home') {
moveCursorLineStart();
} else if (k == 'backspace') {
backspace();
} else if (k == 'tab') {
e.preventDefault();
insertText(' '.repeat(4));
}
}
textInput.oninput = function (e) {
let it = e.inputType;
if (it == 'insertText') {
insertText(e.data);
} else if (it == 'insertLineBreak') {
breakLine();
}
textInput.value = '';
}
textInput.onpaste = onPaste;
gutter.onmousedown = function (e) {
let row = Math.floor((e.clientY - gutter.getBoundingClientRect().top) / lh + parseInt(box.scrollTop / lh));
let pos1 = clipPosToDocument({ row, col: 0 });
let pos2 = clipPosToDocument({ row: row + 1, col: 0 });
select({ row: pos1.row, col: pos1.col }, { row: pos2.row, col: pos2.col });
}
document.onkeyup = function (e) {
let k = e.key.toLowerCase();
if (k in keys) {
keys[k] = false;
}
}
document.onmouseup = function (e) {
if (willClearSelection) {
clearSelection();
let posClipped = clipPosToContainer(e.clientX, e.clientY);
let pos = findPosFromPoints(posClipped.x, posClipped.y);
moveCursorTo(pos.row, pos.col);
}
box.draggable = false;
startSelect = false;
stopAutoScroll();
}
document.onmousemove = function (e) {
if (startSelect) {
let posClipped = clipPosToContainer(e.clientX, e.clientY);
let pos = findPosFromPoints(posClipped.x, posClipped.y);
let rect = box.getBoundingClientRect();
let maxLeft = rect.left + box.clientWidth;
let maxTop = rect.top + box.clientHeight;
offsetXInBox = posClipped.x;
offsetYInBox = posClipped.y;
selectTo(pos.row, pos.col);
if (e.clientX >= maxLeft) {
startScrollRight();
} else {
stopScrollRight();
}
if (e.clientX <= rect.left) {
startScrollLeft();
} else {
stopScrollLeft();
}
if (e.clientY >= maxTop) {
let speed = Math.floor((e.clientY - maxTop) / 10);
startScrollDown(speed);
} else {
stopScrollDown();
}
if (e.clientY <= rect.top) {
let speed = Math.floor((rect.top - e.clientY) / 10);
startScrollUp(speed);
} else {
stopScrollUp();
}
}
}
window.onresize = function () {
renderCount = Math.floor(box.clientHeight / lh) + 2;
renderVisibleLines();
updateSelection();
}
renderVisibleLines();
updateScrollHeight();
updateScrollWidth();
startCursorBlink();
updateLineNumber();
<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="container">
<div class="gutter">
<div class="innerY"></div>
<div class="layer"></div>
</div>
<div class="box">
<div class="marker"></div>
<pre class="text"></pre>
<pre class="font-metrics"></pre>
<div class="scrollbar">
<div class="innerX"></div>
<div class="innerY"></div>
</div>
<div class="cursor"></div>
<textarea spellcheck="false" autofocus="false" class="text-input"></textarea>
</div>
</div>
</body>
</html>
* {
margin: 0;
padding: 0;
}
.container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #23272e;
user-select: none;
overflow: hidden;
}
.gutter {
position: absolute;
left: 0;
top: 0;
overflow: hidden;
height: 100%;
font-size: 13px;
font-family: Consolas;
border-right: 1px solid #3b4048;
box-sizing: border-box;
}
.gutter .item {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 20px;
color: #abb2bf;
text-indent: 10px;
line-height: 20px;
}
.gutter .innerY {
position: absolute;
left: 0;
top: 0;
width: 1px;
opacity: 0;
z-index: -1;
}
.box {
position: absolute;
width: 100%;
height: 100%;
overflow: auto;
}
.box .text {
position: absolute;
left: 0;
top: 0;
font-size: 13px;
font-family: Consolas;
line-height: 20px;
color: #abb2bf;
width: 100%;
z-index: 1;
cursor: text;
}
.box .font-metrics {
position: absolute;
left: 0;
top: 0;
font-size: 13px;
font-family: Consolas;
margin: 0;
padding: 0;
z-index: -1;
opacity: 0;
border: none;
}
.box .innerY {
position: absolute;
left: 0;
top: 0;
width: 1px;
z-index: -1;
opacity: 0;
}
.box .innerX {
position: absolute;
left: 0;
top: 0;
z-index: -1;
opacity: 0;
height: 1px;
}
.box .cursor {
position: absolute;
left: 0;
top: 0;
z-index: 2;
width: 0;
height: 20px;
font-size: 0;
border-left: 2px solid #24acf2;
cursor: text;
}
.box .marker {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.box .marker-item {
position: absolute;
background: rgba(255, 255, 255, 0.2);
}
.box .marker-item .radius {
position: absolute;
top: 0;
width: 7px;
height: 20px;
background: #23272e;
}
.box .marker-item .bg {
background: rgba(255, 255, 255, 0.2);
width: 6px;
}
.box .marker-item .rl {
right: 100%;
}
.box .marker-item .rr {
left: 100%;
}
.box .text-input {
position: absolute;
left: 0;
top: 0;
z-index: 2;
outline: none;
margin: 0;
padding: 0;
width: 1px;
height: 1px;
font-size: 1px;
resize: none;
border: none;
overflow: hidden;
opacity: 0;
}
.box::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.box::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
}
.box::-webkit-scrollbar-corner {
background: transparent;
}
.box::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}