Skip to content

Commit e4e9a50

Browse files
committed
Working multicursor (with bugs when overlapping)
1 parent 9bf0285 commit e4e9a50

6 files changed

Lines changed: 578 additions & 427 deletions

File tree

dist/demo/2025_2/polycope/editor.js

Lines changed: 68 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ContainerSink } from "./caretsink.js";
2-
import { insertAt, deleteAt, distMouseEventToEl, vertDistPointToLineEl, makeid, } from "./helpers.js";
2+
import { insertAt, deleteAt, makeid, } from "./helpers.js";
33
export const editor = (id = Math.random() + "", parentContainerSink, eContext) => {
4-
const { elFromFocusId, calcAndRenderSelection, renderCaret, renderAnchor, getCaretId, setCaretId, getCaretPos, setCaretPos, setAnchorId, setAnchorPos, pushHistory, } = eContext;
4+
const { elFromFocusId, calcAndRenderSelection, setCaretId, setCaretPos, setAnchorId, setAnchorPos, getCursors, } = eContext;
55
const wrapEl = new DOMParser().parseFromString(`<div style="
66
padding: 4px 4px 4px 0;
77
border: 1px solid black;
@@ -11,32 +11,7 @@ export const editor = (id = Math.random() + "", parentContainerSink, eContext) =
1111
display: inline-block;
1212
" class="editor" tabIndex="0"></div>`, "text/html").body.firstChild;
1313
elFromFocusId[id] = wrapEl;
14-
const mousePick = (shouldMoveAnchor) => (e) => {
15-
e.stopPropagation();
16-
const closestLineEl = wrapEl.lineEls.sort((el1, el2) => vertDistPointToLineEl(e, el1) - vertDistPointToLineEl(e, el2))[0];
17-
const picked = [...closestLineEl.children].sort((el1, el2) => distMouseEventToEl(e, el1) - distMouseEventToEl(e, el2))[0];
18-
setCaretId(id);
19-
setCaretPos(picked.id);
20-
if (shouldMoveAnchor) {
21-
setAnchorId(id);
22-
setAnchorPos(picked.id);
23-
renderAnchor();
24-
wrapEl.focus();
25-
}
26-
renderCaret();
27-
calcAndRenderSelection();
28-
};
29-
wrapEl.addEventListener("mousedown", (e) => {
30-
requestAnimationFrame(() => mousePick(true)(e));
31-
e.stopPropagation();
32-
});
33-
wrapEl.addEventListener("mousemove", (e) => {
34-
if (e.buttons === 1) {
35-
requestAnimationFrame(() => mousePick(false)(e));
36-
e.stopPropagation();
37-
}
38-
});
39-
wrapEl.addEventListener("keydown", (e) => {
14+
wrapEl.onKey = (e) => {
4015
if (e.key === "Tab")
4116
e.preventDefault();
4217
if (e.key === "Backspace")
@@ -45,38 +20,33 @@ export const editor = (id = Math.random() + "", parentContainerSink, eContext) =
4520
e.preventDefault();
4621
const discrim = e.key.length === 1 || e.key === "Enter" || e.key === "Backspace";
4722
if (discrim && !e.metaKey) {
48-
pushHistory({
23+
return {
4924
key: e.key,
50-
keyId: makeid(7), // MULTICURSOR BUG! same id for both cursors. uh oh.
51-
});
52-
e.stopPropagation();
25+
keyId: makeid(7),
26+
};
5327
}
5428
else if (e.key === "b" && e.metaKey) {
55-
pushHistory({
29+
return {
5630
key: e.key,
57-
newId: makeid(7), // MULTICURSOR BUG! same id for both cursors. uh oh.
58-
});
59-
e.stopPropagation();
31+
newId: makeid(7),
32+
};
6033
}
6134
else if (e.key === "Tab" && e.shiftKey) {
62-
pushHistory({
35+
return {
6336
despacify: true,
64-
});
65-
e.stopPropagation();
37+
};
6638
}
6739
else if (e.key === "Tab") {
68-
pushHistory({
40+
return {
6941
spacify: true,
70-
});
71-
e.stopPropagation();
42+
};
7243
}
7344
else if (e.key === "/" && e.metaKey) {
74-
pushHistory({
45+
return {
7546
commentify: true,
76-
});
77-
e.stopPropagation();
47+
};
7848
}
79-
});
49+
};
8050
wrapEl.sink = new ContainerSink(() => wrapEl.getBoundingClientRect());
8151
wrapEl.sink.parent = parentContainerSink ?? null;
8252
let str = [];
@@ -85,78 +55,92 @@ export const editor = (id = Math.random() + "", parentContainerSink, eContext) =
8555
str = [];
8656
lines = [[]];
8757
wrapEl.str = str.map((s) => s.char);
58+
wrapEl.rawStr = str;
8859
wrapEl.lines = lines;
8960
wrapEl.innerHTML = "";
9061
}
9162
function myInsertAt(pos, char) {
9263
str = insertAt(str, pos, char);
93-
wrapEl.str = str.map((s) => s.char);
9464
}
95-
// char id to pos
96-
// insertAfter(id, newThing)
97-
// delete(id)
9865
const insertAfter = (id, char) => {
9966
const i = str.findIndex((v) => v.id === id);
10067
if (i === -1)
10168
myInsertAt(0, char);
10269
else
10370
myInsertAt(i + 1, char);
71+
wrapEl.str = str.map((s) => s.char);
72+
wrapEl.rawStr = str;
10473
};
10574
const deleteAtId = (id) => {
10675
const i = str.findIndex((v) => v.id === id);
10776
if (i === -1)
10877
throw "couldn't find id to delete";
10978
str = deleteAt(str, i);
79+
wrapEl.str = str.map((s) => s.char);
80+
wrapEl.rawStr = str;
11081
return i;
11182
};
11283
function act(e) {
113-
if (e.paste) {
114-
if (e.paste.id) {
115-
act({ ...e, paste: undefined, newId: e.paste.id });
116-
// BUG: I think his causes issues, it moves the caret to the wrong spot mid-paste?
117-
elFromFocusId[getCaretId()]?.act({
118-
...e,
119-
paste: e.paste.data,
120-
});
121-
}
122-
else if (Array.isArray(e.paste)) {
123-
for (const entry of e.paste) {
124-
if (entry.id)
125-
act({ ...e, paste: entry });
126-
else
127-
act({ ...e, paste: undefined, key: entry });
128-
}
129-
}
130-
}
131-
else if (e.newId) {
84+
// if (e.paste) {
85+
// if (e.paste.id) {
86+
// act({ ...e, paste: undefined, newId: e.paste.id });
87+
// // BUG: I think his causes issues, it moves the caret to the wrong spot mid-paste?
88+
// elFromFocusId[getCaretId()]?.act({
89+
// ...e,
90+
// paste: e.paste.data,
91+
// });
92+
// } else if (Array.isArray(e.paste)) {
93+
// for (const entry of e.paste) {
94+
// if (entry.id) act({ ...e, paste: entry });
95+
// else act({ ...e, paste: undefined, key: entry });
96+
// }
97+
// }
98+
// } else
99+
if (e.newId) {
132100
const newE = editor(e.newId, wrapEl.sink, eContext);
133101
newE.render();
134-
insertAfter(getCaretPos(), { char: newE, id: e.newId });
135-
setCaretPos(newE.id);
136-
setCaretId(newE.id);
102+
insertAfter(e.adr.caret[1], { char: newE, id: e.newId });
103+
e.setAdr({ ...e.adr, caret: [newE.id, newE.id] });
137104
}
138105
else if (e.key.length === 1) {
139-
//const iid = getCaretPos() === 0 ? id : str[getCaretPos() - 1].id;
140-
insertAfter(getCaretPos(), { char: e.key, id: e.keyId });
141-
setCaretId(id);
142-
setCaretPos(e.keyId);
106+
insertAfter(e.adr.caret[1], { char: e.key, id: e.keyId });
107+
e.setAdr({ ...e.adr, caret: [id, e.keyId] });
143108
}
144109
if (e.key === "Enter") {
145-
insertAfter(getCaretPos(), { char: "\n", id: e.keyId });
146-
setCaretId(id);
147-
setCaretPos(e.keyId);
110+
insertAfter(e.adr.caret[1], { char: "\n", id: e.keyId });
111+
e.setAdr({ ...e.adr, caret: [id, e.keyId] });
148112
}
149113
if (e.key === "Backspace") {
150-
if (getCaretPos() !== id) {
151-
const i = deleteAtId(getCaretPos());
152-
setCaretId(id);
153-
setCaretPos(str[i - 1]?.id ?? id);
114+
const isFirstSink = e.getAdr().caret[1] === id;
115+
if (!isFirstSink) {
116+
const i = deleteAtId(e.getAdr().caret[1]);
117+
// gross, but it works
118+
for (const cursor of getCursors()) {
119+
if (cursor.getAdr().caret[1] === e.adr.caret[1]) {
120+
cursor.setAdr({
121+
...cursor.getAdr(),
122+
caret: [id, str[i - 1]?.id ?? id],
123+
});
124+
}
125+
if (cursor.getAdr().anchor[1] === e.adr.caret[1]) {
126+
cursor.setAdr({
127+
...cursor.getAdr(),
128+
anchor: [id, str[i - 1]?.id ?? id],
129+
});
130+
}
131+
if (cursor.getAdr().carry[1] === e.adr.caret[1]) {
132+
cursor.setAdr({
133+
...cursor.getAdr(),
134+
carry: [id, str[i - 1]?.id ?? id],
135+
});
136+
}
137+
}
138+
//e.setAdr({ ...e.adr, caret: [id, str[i - 1]?.id ?? id] });
154139
}
155140
else {
156141
// TODO?: delete at start of editor
157142
}
158143
}
159-
wrapEl.str = str.map((s) => s.char);
160144
}
161145
function calcLines() {
162146
let curLine = [];

dist/demo/2025_2/polycope/helpers.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const vertDistPointToLineEl = (e, lineEl) => {
5252
const p = e.clientY;
5353
return p >= t && p <= b ? 0 : p <= t ? t - p : p - b;
5454
};
55+
//https://stackoverflow.com/a/1349426
5556
export function makeid(length) {
5657
let result = "";
5758
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

0 commit comments

Comments
 (0)