selection update
This commit is contained in:
@@ -11,6 +11,9 @@
|
||||
margin-right: -1px;
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
.selection {
|
||||
background-color: #b3d7ff;
|
||||
}
|
||||
@keyframes blink {
|
||||
50% {
|
||||
border-color: transparent;
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
export function setupInput(buffer, cursor, onChange) {
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
if (cursor.line > 0) cursor.line--;
|
||||
cursor.col = Math.min(cursor.col, buffer[cursor.line].length);
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
if (cursor.line < buffer.length - 1) cursor.line++;
|
||||
cursor.col = Math.min(cursor.col, buffer[cursor.line].length);
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
if (cursor.col > 0) cursor.col--;
|
||||
else if (cursor.line > 0) {
|
||||
cursor.line--;
|
||||
cursor.col = buffer[cursor.line].length;
|
||||
}
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
if (cursor.col < buffer[cursor.line].length) cursor.col++;
|
||||
else if (cursor.line < buffer.length - 1) {
|
||||
cursor.line++;
|
||||
cursor.col = 0;
|
||||
}
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key === "Backspace") {
|
||||
e.preventDefault();
|
||||
if (cursor.col > 0) {
|
||||
const line = buffer[cursor.line];
|
||||
buffer[cursor.line] = line.slice(0, cursor.col - 1) + line.slice(cursor.col);
|
||||
cursor.col--;
|
||||
} else if (cursor.line > 0) {
|
||||
cursor.col = buffer[cursor.line - 1].length;
|
||||
buffer[cursor.line - 1] += buffer[cursor.line];
|
||||
buffer.splice(cursor.line, 1);
|
||||
cursor.line--;
|
||||
}
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const line = buffer[cursor.line];
|
||||
const rest = line.slice(cursor.col);
|
||||
buffer[cursor.line] = line.slice(0, cursor.col);
|
||||
buffer.splice(cursor.line + 1, 0, rest);
|
||||
cursor.line++;
|
||||
cursor.col = 0;
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key.length === 1) {
|
||||
e.preventDefault();
|
||||
const line = buffer[cursor.line];
|
||||
buffer[cursor.line] = line.slice(0, cursor.col) + e.key + line.slice(cursor.col);
|
||||
cursor.col++;
|
||||
onChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export function moveUp(buffer, cursor) {
|
||||
if (cursor.line > 0) cursor.line--;
|
||||
cursor.col = Math.min(cursor.col, buffer[cursor.line].length);
|
||||
}
|
||||
|
||||
export function moveDown(buffer, cursor) {
|
||||
if (cursor.line < buffer.length - 1) cursor.line++;
|
||||
cursor.col = Math.min(cursor.col, buffer[cursor.line].length);
|
||||
}
|
||||
|
||||
export function moveLeft(buffer, cursor) {
|
||||
if (cursor.col > 0) cursor.col--;
|
||||
else if (cursor.line > 0) {
|
||||
cursor.line--;
|
||||
cursor.col = buffer[cursor.line].length;
|
||||
}
|
||||
}
|
||||
|
||||
export function moveRight(buffer, cursor) {
|
||||
if (cursor.col < buffer[cursor.line].length) cursor.col++;
|
||||
else if (cursor.line < buffer.length - 1) {
|
||||
cursor.line++;
|
||||
cursor.col = 0;
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export function escapeChar(c) {
|
||||
if (c === "&") return "&";
|
||||
if (c === "<") return "<";
|
||||
if (c === ">") return ">";
|
||||
return c;
|
||||
}
|
||||
|
||||
export function escapeHtml(s) {
|
||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
import { moveUp, moveDown, moveLeft, moveRight } from "./cursor.js";
|
||||
import { hasSelection, selectedRange, clearSelection, deleteSelection } from "./selection.js";
|
||||
import { elementToPos } from "./mouse.js";
|
||||
|
||||
export function setupInput(buffer, cursor, selection, onChange) {
|
||||
let isMouseDown = false;
|
||||
|
||||
function startDrag(e) {
|
||||
if (!e.target.closest("#text")) return;
|
||||
isMouseDown = true;
|
||||
const pos = elementToPos(e.clientX, e.clientY, buffer);
|
||||
cursor.line = pos.line;
|
||||
cursor.col = pos.col;
|
||||
selection.anchor = { ...pos };
|
||||
selection.head = { ...pos };
|
||||
onChange();
|
||||
}
|
||||
|
||||
function drag(e) {
|
||||
if (!isMouseDown) return;
|
||||
const pos = elementToPos(e.clientX, e.clientY, buffer);
|
||||
cursor.line = pos.line;
|
||||
cursor.col = pos.col;
|
||||
selection.head = { ...pos };
|
||||
onChange();
|
||||
}
|
||||
|
||||
function endDrag() {
|
||||
isMouseDown = false;
|
||||
}
|
||||
|
||||
function selectWord(e) {
|
||||
if (!e.target.closest("#text")) return;
|
||||
const pos = elementToPos(e.clientX, e.clientY, buffer);
|
||||
const line = buffer[pos.line];
|
||||
let start = pos.col;
|
||||
let end = pos.col;
|
||||
while (start > 0 && /\w/.test(line[start - 1])) start--;
|
||||
while (end < line.length && /\w/.test(line[end])) end++;
|
||||
cursor.line = pos.line;
|
||||
cursor.col = end;
|
||||
selection.anchor = { line: pos.line, col: start };
|
||||
selection.head = { line: pos.line, col: end };
|
||||
onChange();
|
||||
}
|
||||
|
||||
function moveCursor(newLine, newCol, extending) {
|
||||
if (extending) {
|
||||
if (selection.anchor === null)
|
||||
selection.anchor = { line: cursor.line, col: cursor.col };
|
||||
selection.head = { line: newLine, col: newCol };
|
||||
} else {
|
||||
clearSelection(selection);
|
||||
}
|
||||
cursor.line = newLine;
|
||||
cursor.col = newCol;
|
||||
}
|
||||
|
||||
function getSelectedText() {
|
||||
const range = selectedRange(selection);
|
||||
if (!range) return "";
|
||||
const { start, end } = range;
|
||||
if (start.line === end.line)
|
||||
return buffer[start.line].slice(start.col, end.col);
|
||||
const lines = [buffer[start.line].slice(start.col)];
|
||||
for (let i = start.line + 1; i < end.line; i++)
|
||||
lines.push(buffer[i]);
|
||||
lines.push(buffer[end.line].slice(0, end.col));
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function insertText(text) {
|
||||
if (hasSelection(selection)) deleteSelection(buffer, cursor, selection);
|
||||
const pasteLines = text.split("\n");
|
||||
const line = buffer[cursor.line];
|
||||
const before = line.slice(0, cursor.col);
|
||||
const after = line.slice(cursor.col);
|
||||
buffer[cursor.line] = before + pasteLines[0] + after;
|
||||
for (let i = 1; i < pasteLines.length; i++)
|
||||
buffer.splice(cursor.line + i, 0, pasteLines[i]);
|
||||
cursor.line += pasteLines.length - 1;
|
||||
cursor.col = pasteLines[pasteLines.length - 1].length +
|
||||
(pasteLines.length === 1 ? before.length : 0);
|
||||
clearSelection(selection);
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
selection.anchor = { line: 0, col: 0 };
|
||||
selection.head = { line: buffer.length - 1, col: buffer[buffer.length - 1].length };
|
||||
cursor.line = buffer.length - 1;
|
||||
cursor.col = buffer[buffer.length - 1].length;
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", startDrag);
|
||||
document.addEventListener("mousemove", drag);
|
||||
document.addEventListener("mouseup", endDrag);
|
||||
document.addEventListener("dblclick", selectWord);
|
||||
|
||||
document.addEventListener("paste", (e) => {
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData.getData("text");
|
||||
if (!text) return;
|
||||
insertText(text);
|
||||
onChange();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const mod = e.ctrlKey || e.metaKey;
|
||||
const extending = e.shiftKey;
|
||||
|
||||
if (mod) {
|
||||
if (e.key === "c") {
|
||||
e.preventDefault();
|
||||
const text = getSelectedText();
|
||||
if (text) navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
if (e.key === "x") {
|
||||
e.preventDefault();
|
||||
const text = getSelectedText();
|
||||
if (text) navigator.clipboard.writeText(text);
|
||||
deleteSelection(buffer, cursor, selection);
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key === "v") {
|
||||
return;
|
||||
}
|
||||
if (e.key === "a") {
|
||||
e.preventDefault();
|
||||
selectAll();
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
const newLine = Math.max(0, cursor.line - 1);
|
||||
const newCol = Math.min(cursor.col, buffer[newLine].length);
|
||||
moveCursor(newLine, newCol, extending);
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
const newLine = Math.min(buffer.length - 1, cursor.line + 1);
|
||||
const newCol = Math.min(cursor.col, buffer[newLine].length);
|
||||
moveCursor(newLine, newCol, extending);
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
let newLine = cursor.line;
|
||||
let newCol = cursor.col;
|
||||
if (newCol > 0) newCol--;
|
||||
else if (newLine > 0) { newLine--; newCol = buffer[newLine].length; }
|
||||
moveCursor(newLine, newCol, extending);
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
let newLine = cursor.line;
|
||||
let newCol = cursor.col;
|
||||
if (newCol < buffer[cursor.line].length) newCol++;
|
||||
else if (newLine < buffer.length - 1) { newLine++; newCol = 0; }
|
||||
moveCursor(newLine, newCol, extending);
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key === "Backspace") {
|
||||
e.preventDefault();
|
||||
if (hasSelection(selection)) {
|
||||
deleteSelection(buffer, cursor, selection);
|
||||
} else if (cursor.col > 0) {
|
||||
const line = buffer[cursor.line];
|
||||
buffer[cursor.line] = line.slice(0, cursor.col - 1) + line.slice(cursor.col);
|
||||
cursor.col--;
|
||||
} else if (cursor.line > 0) {
|
||||
cursor.col = buffer[cursor.line - 1].length;
|
||||
buffer[cursor.line - 1] += buffer[cursor.line];
|
||||
buffer.splice(cursor.line, 1);
|
||||
cursor.line--;
|
||||
}
|
||||
clearSelection(selection);
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (hasSelection(selection)) deleteSelection(buffer, cursor, selection);
|
||||
const line = buffer[cursor.line];
|
||||
const rest = line.slice(cursor.col);
|
||||
buffer[cursor.line] = line.slice(0, cursor.col);
|
||||
buffer.splice(cursor.line + 1, 0, rest);
|
||||
cursor.line++;
|
||||
cursor.col = 0;
|
||||
clearSelection(selection);
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
if (e.key.length === 1) {
|
||||
e.preventDefault();
|
||||
if (hasSelection(selection)) deleteSelection(buffer, cursor, selection);
|
||||
const line = buffer[cursor.line];
|
||||
buffer[cursor.line] = line.slice(0, cursor.col) + e.key + line.slice(cursor.col);
|
||||
cursor.col++;
|
||||
clearSelection(selection);
|
||||
onChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { escapeChar } from "./html.js";
|
||||
|
||||
function bufferColToDomOffset(lineText, col) {
|
||||
let offset = 0;
|
||||
for (let i = 0; i < col && i < lineText.length; i++)
|
||||
offset += escapeChar(lineText[i]).length;
|
||||
return offset;
|
||||
}
|
||||
|
||||
function findTextNode(parent, targetOffset) {
|
||||
let remaining = targetOffset;
|
||||
const walker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT);
|
||||
let node;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (remaining <= node.textContent.length)
|
||||
return { node, offset: remaining };
|
||||
remaining -= node.textContent.length;
|
||||
}
|
||||
return { node: parent, offset: parent.childNodes.length };
|
||||
}
|
||||
|
||||
function posToElement(line, col, buffer) {
|
||||
const spans = document.querySelectorAll("#text > span");
|
||||
if (line >= spans.length) return null;
|
||||
const span = spans[line];
|
||||
const range = document.createRange();
|
||||
const lineText = buffer[line] || "";
|
||||
const offset = bufferColToDomOffset(lineText, col);
|
||||
const tn = findTextNode(span, offset);
|
||||
range.setStart(tn.node, tn.offset);
|
||||
range.collapse(true);
|
||||
const rect = range.getBoundingClientRect();
|
||||
return { x: rect.left, y: rect.top, height: rect.height };
|
||||
}
|
||||
|
||||
export function elementToPos(x, y, buffer) {
|
||||
const spans = document.querySelectorAll("#text > span");
|
||||
let bestLine = 0;
|
||||
let bestDistY = Infinity;
|
||||
|
||||
for (let i = 0; i < spans.length; i++) {
|
||||
const rect = spans[i].getBoundingClientRect();
|
||||
const midY = rect.top + rect.height / 2;
|
||||
const distY = Math.abs(y - midY);
|
||||
if (distY < bestDistY) {
|
||||
bestDistY = distY;
|
||||
bestLine = i;
|
||||
}
|
||||
}
|
||||
|
||||
let bestCol = 0;
|
||||
let bestDistX = Infinity;
|
||||
const lineText = buffer[bestLine] || "";
|
||||
for (let c = 0; c <= lineText.length; c++) {
|
||||
const pos = posToElement(bestLine, c, buffer);
|
||||
if (!pos) continue;
|
||||
const dist = Math.abs(x - pos.x);
|
||||
if (dist < bestDistX) {
|
||||
bestDistX = dist;
|
||||
bestCol = c;
|
||||
}
|
||||
}
|
||||
|
||||
return { line: bestLine, col: bestCol };
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
export function render(buffer, cursor, selection) {
|
||||
const range = selectedRange(selection);
|
||||
|
||||
const html = buffer.map((line, i) => {
|
||||
const chars = [];
|
||||
for (let c = 0; c <= line.length; c++) {
|
||||
if (i === cursor.line && c === cursor.col)
|
||||
chars.push(`<span class="cursor"></span>`);
|
||||
if (c < line.length) {
|
||||
const inSel = range &&
|
||||
(i > range.start.line || (i === range.start.line && c >= range.start.col)) &&
|
||||
(i < range.end.line || (i === range.end.line && c < range.end.col));
|
||||
const ch = escapeChar(line[c]);
|
||||
chars.push(inSel ? `<span class="selection">${ch}</span>` : ch);
|
||||
}
|
||||
}
|
||||
if (i === cursor.line && cursor.col >= line.length)
|
||||
chars.push(`<span class="cursor"></span>`);
|
||||
return `<span>${chars.join("")}</span>`;
|
||||
});
|
||||
|
||||
document.getElementById("text").innerHTML = html.join("<br>");
|
||||
}
|
||||
|
||||
function selectedRange(selection) {
|
||||
if (selection.anchor === null || selection.head === null) return null;
|
||||
const a = selection.anchor;
|
||||
const h = selection.head;
|
||||
if (a.line === h.line && a.col === h.col) return null;
|
||||
if (a.line < h.line || (a.line === h.line && a.col < h.col))
|
||||
return { start: { ...a }, end: { ...h } };
|
||||
return { start: { ...h }, end: { ...a } };
|
||||
}
|
||||
|
||||
function escapeChar(c) {
|
||||
if (c === "&") return "&";
|
||||
if (c === "<") return "<";
|
||||
if (c === ">") return ">";
|
||||
return c;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
export function hasSelection(selection) {
|
||||
return selection.anchor !== null &&
|
||||
(selection.anchor.line !== selection.head.line ||
|
||||
selection.anchor.col !== selection.head.col);
|
||||
}
|
||||
|
||||
export function selectedRange(selection) {
|
||||
if (selection.anchor === null || selection.head === null) return null;
|
||||
const a = selection.anchor;
|
||||
const h = selection.head;
|
||||
if (a.line === h.line && a.col === h.col) return null;
|
||||
if (a.line < h.line || (a.line === h.line && a.col < h.col))
|
||||
return { start: { ...a }, end: { ...h } };
|
||||
return { start: { ...h }, end: { ...a } };
|
||||
}
|
||||
|
||||
export function clearSelection(selection) {
|
||||
selection.anchor = null;
|
||||
selection.head = null;
|
||||
}
|
||||
|
||||
export function deleteSelection(buffer, cursor, selection) {
|
||||
const range = selectedRange(selection);
|
||||
if (!range) return;
|
||||
const { start, end } = range;
|
||||
if (start.line === end.line) {
|
||||
const line = buffer[start.line];
|
||||
buffer[start.line] = line.slice(0, start.col) + line.slice(end.col);
|
||||
} else {
|
||||
const before = buffer[start.line].slice(0, start.col);
|
||||
const after = buffer[end.line].slice(end.col);
|
||||
buffer[start.line] = before + after;
|
||||
buffer.splice(start.line + 1, end.line - start.line);
|
||||
}
|
||||
cursor.line = start.line;
|
||||
cursor.col = start.col;
|
||||
clearSelection(selection);
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { render } from "./render.js";
|
||||
import { setupInput } from "./input.js";
|
||||
import { render } from "./lib/render.js";
|
||||
import { setupInput } from "./lib/keyboard.js";
|
||||
|
||||
const buffer = ["Hello, world!", "Second line."];
|
||||
const cursor = { line: 0, col: 0 };
|
||||
const selection = { anchor: null, head: null };
|
||||
|
||||
function onChange() {
|
||||
render(buffer, cursor);
|
||||
render(buffer, cursor, selection);
|
||||
try {
|
||||
console.log(buffer.map((line) => JSON.parse(App.testing(line))));
|
||||
} catch (e) {
|
||||
@@ -14,7 +15,5 @@ function onChange() {
|
||||
document.getElementById("text").innerHTML += "<br>";
|
||||
}
|
||||
|
||||
function escapeHtml(s) {}
|
||||
|
||||
setupInput(buffer, cursor, onChange);
|
||||
render(buffer, cursor);
|
||||
setupInput(buffer, cursor, selection, onChange);
|
||||
render(buffer, cursor, selection);
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export function render(buffer, cursor) {
|
||||
const html = buffer.map((line, i) => {
|
||||
if (i !== cursor.line) return `<span>${escapeHtml(line)}</span>`;
|
||||
const before = escapeHtml(line.slice(0, cursor.col));
|
||||
const after = escapeHtml(line.slice(cursor.col));
|
||||
return `<span>${before}<span class="cursor"></span>${after}</span>`;
|
||||
});
|
||||
document.getElementById("text").innerHTML = html.join("<br>");
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
Reference in New Issue
Block a user