@pfmcodes/caret 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +485 -379
- package/components/caret.js +95 -0
- package/components/font.js +16 -0
- package/components/lineCounter.js +91 -0
- package/components/textEditor.js +503 -0
- package/index.js +4 -19
- package/package.json +3 -13
- package/utilities.js +32 -0
- package/editor.js +0 -470
- package/index.css +0 -110
- package/theme.js +0 -14
- package/types/editor.ts +0 -481
- package/types/index.d.ts +0 -46
- package/types/index.ts +0 -22
- package/types/langauges.ts +0 -116
- package/types/theme.ts +0 -14
- /package/{langauges.js → components/languages.js} +0 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createTextEditor {
|
|
3
|
+
* @param {HTMLElement} parent - The element to append the editor to
|
|
4
|
+
* @param {string} content - Initial content of the editor, default is ""
|
|
5
|
+
* @param {string|number} id - Unique ID for this instance, used for undo/redo stack namespacing
|
|
6
|
+
* @param {Object} options - Optional configuration {
|
|
7
|
+
* @param {boolean} dark - dark theme enabled, false by default
|
|
8
|
+
* @param {boolean} shadow - box shadow enabled, true by default
|
|
9
|
+
* @param {string} focusColor - border color on focus, default #7c3aed
|
|
10
|
+
* @param {string} shadowColor - shadow color, default #000
|
|
11
|
+
* @param {boolean} lock - read-only mode, false by default
|
|
12
|
+
* @param {string} language - highlight.js language, default "plaintext"
|
|
13
|
+
* @param {string} hlTheme - highlight.js theme name, default "hybrid"
|
|
14
|
+
* @param {Object} font {
|
|
15
|
+
* @param {string} url - font file URL, only needed for external/custom fonts
|
|
16
|
+
* @param {string} name - font name, required for custom fonts
|
|
17
|
+
* }
|
|
18
|
+
* @param {Object} theme - custom colors {
|
|
19
|
+
* @param {Object} dark {
|
|
20
|
+
* @param {string} background.editor - editor background color
|
|
21
|
+
* @param {string} background.lineCounter - line counter background color
|
|
22
|
+
* @param {string} color.editor - editor text color
|
|
23
|
+
* @param {string} color.lineCounter - line counter text color
|
|
24
|
+
* @param {string} editor.caret - caret color
|
|
25
|
+
* }
|
|
26
|
+
* @param {Object} light {
|
|
27
|
+
* @param {string} background.editor - editor background color
|
|
28
|
+
* @param {string} background.lineCounter - line counter background color
|
|
29
|
+
* @param {string} color.editor - editor text color
|
|
30
|
+
* @param {string} color.lineCounter - line counter text color
|
|
31
|
+
* @param {string} editor.caret - caret color
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* @returns {Promise<Object>} {
|
|
36
|
+
* @return {function} getValue - returns current editor content (strips \u200B)
|
|
37
|
+
* @return {function} setValue - (@param {string} val) sets editor content
|
|
38
|
+
* @return {function} getCursor - returns current cursor offset
|
|
39
|
+
* @return {function} setCursor - (@param {number} pos) moves cursor to position
|
|
40
|
+
* @return {function} undo - undoes last change, restores cursor position
|
|
41
|
+
* @return {function} redo - redoes last undone change, restores cursor position
|
|
42
|
+
* @return {function} onChange - (@param {function} fn) fires on every content change with new text
|
|
43
|
+
* @return {function} onCursorMove - (@param {function} fn) fires on every cursor move with new position
|
|
44
|
+
* @return {function} isFocused - returns true if editor is currently focused
|
|
45
|
+
* @return {function} setLanguage - (@param {string} lang) switches syntax highlighting language
|
|
46
|
+
* @return {string} id - the editor instance id
|
|
47
|
+
* @return {function} delete - destroys editor, removes DOM elements and event listeners
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* @notes
|
|
51
|
+
* - Requires Chrome/Chromium — uses EditContext API (not supported in Firefox/Safari yet)
|
|
52
|
+
* - Undo/redo stacks accessible globally via window.caret[`undoStack.${id}`]
|
|
53
|
+
* - \u200B (zero-width space) used internally for newline rendering, stripped from getValue()
|
|
54
|
+
* - Keyboard shortcuts: Ctrl+Z undo, Ctrl+Y / Ctrl+Shift+Z redo, Tab indent, Shift+Tab unindent
|
|
55
|
+
* }
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import { createLineCounter, updateLineCounter } from "./lineCounter.js";
|
|
59
|
+
import { loadFont } from "./font.js";
|
|
60
|
+
import { createCaret } from "./caret.js";
|
|
61
|
+
// @ts-ignore
|
|
62
|
+
import hljs from "https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/core.js";
|
|
63
|
+
import languages from "./languages.js";
|
|
64
|
+
|
|
65
|
+
languages.init();
|
|
66
|
+
|
|
67
|
+
async function createTextEditor(parent, content = "", id, options = {}) {
|
|
68
|
+
let onCursorMoveFn = null;
|
|
69
|
+
async function isChromiumEngine() {
|
|
70
|
+
if (navigator.userAgentData) {
|
|
71
|
+
return navigator.userAgentData.brands.some(b => b.brand === 'Chromium');
|
|
72
|
+
}
|
|
73
|
+
const ua = navigator.userAgent;
|
|
74
|
+
return /Chrome/i.test(ua) && !/Edg/i.test(ua) && !/OPR/i.test(ua);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getBrowserName() {
|
|
78
|
+
if (navigator.userAgentData) {
|
|
79
|
+
const brands = navigator.userAgentData.brands;
|
|
80
|
+
const primaryBrand = brands.find(b => b.brand !== 'Chromium' && b.brand !== 'Not(A:Brand)') || brands[0];
|
|
81
|
+
return primaryBrand.brand;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ua = navigator.userAgent;
|
|
85
|
+
if (ua.includes("Firefox")) return "Mozilla Firefox";
|
|
86
|
+
if (ua.includes("SamsungBrowser")) return "Samsung Internet";
|
|
87
|
+
if (ua.includes("Opera") || ua.includes("OPR")) return "Opera";
|
|
88
|
+
if (ua.includes("Trident")) return "Internet Explorer";
|
|
89
|
+
if (ua.includes("Edge")) return "Microsoft Edge (Legacy)";
|
|
90
|
+
if (ua.includes("Edg")) return "Microsoft Edge";
|
|
91
|
+
if (ua.includes("Chrome")) return "Google Chrome";
|
|
92
|
+
if (ua.includes("Safari")) return "Apple Safari";
|
|
93
|
+
|
|
94
|
+
return "Unknown Browser";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
isChromiumEngine().then(async isChromium => {
|
|
98
|
+
if (!isChromium) {
|
|
99
|
+
const main = document.createElement("div");
|
|
100
|
+
main.style = "display: flex; align-items: center; justify-content: center; padding: 20px; white-space: pre-wrap; margin: 0 auto;";
|
|
101
|
+
main.innerHTML = `<h2>Caret (editor engine) does not yet support ${await getBrowserName()}.<br>File an issue <a href="https://github.com/PFMCODES/Caret/issues">here.</a></h2>`;
|
|
102
|
+
parent.appendChild(main);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
if (id === undefined || id === null || (typeof id !== "string" && typeof id !== "number")) {
|
|
106
|
+
console.error(`parameter 'id' of function createTextEditor must not be '${typeof id}', it must be a number or string`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!parent || !(parent instanceof HTMLElement)) {
|
|
110
|
+
console.error(`'parent' parameter of function 'createTextEditor' must be an HTMLElement`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!("EditContext" in window)) {
|
|
114
|
+
console.error("EditContext API is not supported in ", await getBrowserName());
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!window.caret) window.caret = {};
|
|
119
|
+
window.caret[`undoStack.${id}`] = [{ content, cursor: 0 }];
|
|
120
|
+
window.caret[`redoStack.${id}`] = [];
|
|
121
|
+
|
|
122
|
+
const lock = options.lock || false;
|
|
123
|
+
const focusColor = options.focusColor || '#7c3aed';
|
|
124
|
+
const dark = options.dark || false;
|
|
125
|
+
const boxShadow = options.shadow ?? true;
|
|
126
|
+
const shadowColor = options.shadowColor || "#000";
|
|
127
|
+
const theme = options.theme;
|
|
128
|
+
const font = options.font || {};
|
|
129
|
+
let language = options.language || "plaintext";
|
|
130
|
+
|
|
131
|
+
const themeLink = document.createElement("link");
|
|
132
|
+
themeLink.rel = "stylesheet";
|
|
133
|
+
themeLink.id = `caret-theme-${id}`;
|
|
134
|
+
themeLink.href = options.hlTheme
|
|
135
|
+
? `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/${options.hlTheme}.css`
|
|
136
|
+
: `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/hybrid.css`;
|
|
137
|
+
document.head.appendChild(themeLink);
|
|
138
|
+
|
|
139
|
+
if (!languages.registeredLanguages.includes(language)) {
|
|
140
|
+
const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${language}.js`);
|
|
141
|
+
languages.registerLanguage(language, mod.default);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let fontName;
|
|
145
|
+
if (!font.url && !font.name) {
|
|
146
|
+
fontName = "monospace";
|
|
147
|
+
parent.style.fontFamily = fontName;
|
|
148
|
+
} else if (!font.url && font.name) {
|
|
149
|
+
parent.style.fontFamily = font.name;
|
|
150
|
+
} else {
|
|
151
|
+
fontName = font.name;
|
|
152
|
+
loadFont(fontName, font.url);
|
|
153
|
+
parent.style.fontFamily = fontName;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let text = content;
|
|
157
|
+
let selStart = content.length;
|
|
158
|
+
let selEnd = content.length;
|
|
159
|
+
let isFocused = false;
|
|
160
|
+
let onChangeFn = null;
|
|
161
|
+
|
|
162
|
+
const editContext = new EditContext({
|
|
163
|
+
text,
|
|
164
|
+
selectionStart: selStart,
|
|
165
|
+
selectionEnd: selEnd
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const main = document.createElement("div");
|
|
169
|
+
main.editContext = editContext;
|
|
170
|
+
main.tabIndex = 0;
|
|
171
|
+
main.style.whiteSpace = "pre";
|
|
172
|
+
main.style.height = "100%";
|
|
173
|
+
main.style.width = "100%";
|
|
174
|
+
main.style.minWidth = "50px";
|
|
175
|
+
main.style.minHeight = "25px";
|
|
176
|
+
main.style.fontSize = "14px";
|
|
177
|
+
main.style.lineHeight = "1.5";
|
|
178
|
+
main.style.outline = "none";
|
|
179
|
+
main.style.boxSizing = "border-box";
|
|
180
|
+
main.style.borderTopRightRadius = "5px";
|
|
181
|
+
main.style.borderBottomRightRadius = "5px";
|
|
182
|
+
main.style.transition = "all 0.2s ease-in-out";
|
|
183
|
+
main.style.display = "block";
|
|
184
|
+
main.style.paddingTop = "5px";
|
|
185
|
+
main.style.overflowX = "auto";
|
|
186
|
+
main.style.scrollbarWidth = "none";
|
|
187
|
+
main.style.cursor = "text";
|
|
188
|
+
main.style.userSelect = "none";
|
|
189
|
+
main.style.caretColor = "transparent";
|
|
190
|
+
|
|
191
|
+
if (boxShadow) main.style.boxShadow = `1px 1px 1px 1px ${shadowColor}`;
|
|
192
|
+
|
|
193
|
+
if (!theme) {
|
|
194
|
+
main.style.background = dark ? "#101010" : "#d4d4d4";
|
|
195
|
+
main.style.color = dark ? "#fff" : "#000";
|
|
196
|
+
} else {
|
|
197
|
+
main.style.background = dark ? theme.dark["background.editor"] : theme.light["background.editor"];
|
|
198
|
+
main.style.color = dark ? theme.dark["color.editor"] : theme.light["color.editor"];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let caretColor;
|
|
202
|
+
if (options.theme) {
|
|
203
|
+
caretColor = dark ? options.theme.dark["editor.caret"] : options.theme.light["editor.caret"];
|
|
204
|
+
} else {
|
|
205
|
+
caretColor = "#fff";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
parent.style.display = "flex";
|
|
209
|
+
parent.style.alignItems = "flex-start";
|
|
210
|
+
parent.style.border = "2px solid #0000";
|
|
211
|
+
parent.style.padding = "5px";
|
|
212
|
+
parent.style.position = "relative";
|
|
213
|
+
|
|
214
|
+
let lineCounter;
|
|
215
|
+
lineCounter = await createLineCounter(parent, content.split("\n").length, id, options);
|
|
216
|
+
|
|
217
|
+
parent.appendChild(main);
|
|
218
|
+
|
|
219
|
+
const caret = createCaret(parent, main, { ...options, caretColor });
|
|
220
|
+
|
|
221
|
+
function render() {
|
|
222
|
+
let displayText = text;
|
|
223
|
+
let od = 0;
|
|
224
|
+
if (displayText.endsWith("\n")) {
|
|
225
|
+
displayText += "\u200B";
|
|
226
|
+
od = 1;
|
|
227
|
+
}
|
|
228
|
+
const highlighted = hljs.highlight(displayText, { language }).value;
|
|
229
|
+
main.innerHTML = highlighted;
|
|
230
|
+
|
|
231
|
+
updateLineCounter(lineCounter, text.trimEnd().split("\n").length + od);
|
|
232
|
+
caret.update(selStart);
|
|
233
|
+
if (onChangeFn) onChangeFn(text);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function saveState() {
|
|
237
|
+
const stack = window.caret[`undoStack.${id}`];
|
|
238
|
+
if (text !== stack[stack.length - 1]?.content) {
|
|
239
|
+
stack.push({ content: text, cursor: selStart });
|
|
240
|
+
window.caret[`redoStack.${id}`] = [];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function undo() {
|
|
245
|
+
const stack = window.caret[`undoStack.${id}`];
|
|
246
|
+
const redoStack = window.caret[`redoStack.${id}`];
|
|
247
|
+
if (stack.length <= 1) return;
|
|
248
|
+
const current = stack.pop();
|
|
249
|
+
redoStack.push(current);
|
|
250
|
+
const prev = stack[stack.length - 1];
|
|
251
|
+
text = prev.content;
|
|
252
|
+
const diff = current.content.length - prev.content.length;
|
|
253
|
+
selStart = selEnd = Math.max(0, current.cursor - diff);
|
|
254
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
255
|
+
editContext.updateSelection(selStart, selEnd);
|
|
256
|
+
render();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function redo() {
|
|
260
|
+
const stack = window.caret[`undoStack.${id}`];
|
|
261
|
+
const redoStack = window.caret[`redoStack.${id}`];
|
|
262
|
+
if (redoStack.length === 0) return;
|
|
263
|
+
const next = redoStack.pop();
|
|
264
|
+
stack.push(next);
|
|
265
|
+
text = next.content;
|
|
266
|
+
selStart = selEnd = next.cursor;
|
|
267
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
268
|
+
editContext.updateSelection(selStart, selEnd);
|
|
269
|
+
render();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
editContext.addEventListener("textupdate", (e) => {
|
|
273
|
+
if (lock) return;
|
|
274
|
+
text = text.slice(0, e.updateRangeStart) + e.text + text.slice(e.updateRangeEnd);
|
|
275
|
+
text = text.replaceAll("\u200B", "");
|
|
276
|
+
selStart = selEnd = e.selectionStart;
|
|
277
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
278
|
+
saveState();
|
|
279
|
+
render();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
editContext.addEventListener("selectionchange", (e) => {
|
|
283
|
+
selStart = e.selectionStart;
|
|
284
|
+
selEnd = e.selectionEnd;
|
|
285
|
+
caret.update(selStart);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
main.addEventListener("keydown", (e) => {
|
|
289
|
+
if (lock) return;
|
|
290
|
+
|
|
291
|
+
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === "z") {
|
|
292
|
+
e.preventDefault();
|
|
293
|
+
undo();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "y") {
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
redo();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "Z") {
|
|
302
|
+
e.preventDefault();
|
|
303
|
+
redo();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (e.key === "Tab" && !e.shiftKey) {
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
const indent = " ";
|
|
310
|
+
if (selStart !== selEnd) {
|
|
311
|
+
const before = text.slice(0, selStart);
|
|
312
|
+
const selected = text.slice(selStart, selEnd);
|
|
313
|
+
const after = text.slice(selEnd);
|
|
314
|
+
const indented = selected.split("\n").map(l => indent + l).join("\n");
|
|
315
|
+
text = before + indented + after;
|
|
316
|
+
selEnd = selStart + indented.length;
|
|
317
|
+
} else {
|
|
318
|
+
text = text.slice(0, selStart) + indent + text.slice(selStart);
|
|
319
|
+
selStart = selEnd = selStart + indent.length;
|
|
320
|
+
}
|
|
321
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
322
|
+
editContext.updateSelection(selStart, selEnd);
|
|
323
|
+
saveState();
|
|
324
|
+
render();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (e.key === "Enter") {
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
const newText = text.slice(0, selStart) + "\n" + "\u200B" + text.slice(selEnd);
|
|
331
|
+
text = newText;
|
|
332
|
+
selStart = selEnd = selStart + 1;
|
|
333
|
+
editContext.updateText(0, text.length, text);
|
|
334
|
+
editContext.updateSelection(selStart, selEnd);
|
|
335
|
+
saveState();
|
|
336
|
+
render();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (e.shiftKey && e.key === "Tab") {
|
|
341
|
+
e.preventDefault();
|
|
342
|
+
if (selStart !== selEnd) {
|
|
343
|
+
const before = text.slice(0, selStart);
|
|
344
|
+
const selected = text.slice(selStart, selEnd);
|
|
345
|
+
const after = text.slice(selEnd);
|
|
346
|
+
const unindented = selected.split("\n").map(l =>
|
|
347
|
+
l.startsWith(" ") ? l.slice(4) :
|
|
348
|
+
l.startsWith("\t") ? l.slice(1) : l
|
|
349
|
+
).join("\n");
|
|
350
|
+
text = before + unindented + after;
|
|
351
|
+
selEnd = selStart + unindented.length;
|
|
352
|
+
} else {
|
|
353
|
+
const lineStart = text.lastIndexOf("\n", selStart - 1) + 1;
|
|
354
|
+
const linePrefix = text.slice(lineStart, lineStart + 4);
|
|
355
|
+
if (linePrefix === " ") {
|
|
356
|
+
text = text.slice(0, lineStart) + text.slice(lineStart + 4);
|
|
357
|
+
selStart = selEnd = Math.max(lineStart, selStart - 4);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
361
|
+
editContext.updateSelection(selStart, selEnd);
|
|
362
|
+
saveState();
|
|
363
|
+
render();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (e.key === "ArrowLeft") {
|
|
367
|
+
e.preventDefault();
|
|
368
|
+
selStart = selEnd = Math.max(0, selStart - 1);
|
|
369
|
+
editContext.updateSelection(selStart, selEnd);
|
|
370
|
+
caret.update(selStart);
|
|
371
|
+
if (onCursorMoveFn) onCursorMoveFn(selStart);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (e.key === "ArrowRight") {
|
|
376
|
+
e.preventDefault();
|
|
377
|
+
selStart = selEnd = Math.min(text.length, selStart + 1);
|
|
378
|
+
editContext.updateSelection(selStart, selEnd);
|
|
379
|
+
caret.update(selStart);
|
|
380
|
+
if (onCursorMoveFn) onCursorMoveFn(selStart);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (e.key === "ArrowUp") {
|
|
385
|
+
e.preventDefault();
|
|
386
|
+
const lineStart = text.lastIndexOf("\n", selStart - 1) + 1;
|
|
387
|
+
const prevLineEnd = lineStart - 1;
|
|
388
|
+
const prevLineStart = text.lastIndexOf("\n", prevLineEnd - 1) + 1;
|
|
389
|
+
const col = selStart - lineStart;
|
|
390
|
+
const prevLineLength = prevLineEnd - prevLineStart;
|
|
391
|
+
selStart = selEnd = prevLineStart + Math.min(col, prevLineLength);
|
|
392
|
+
editContext.updateSelection(selStart, selEnd);
|
|
393
|
+
caret.update(selStart);
|
|
394
|
+
if (onCursorMoveFn) onCursorMoveFn(selStart);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (e.key === "ArrowDown") {
|
|
399
|
+
e.preventDefault();
|
|
400
|
+
const lineStart = text.lastIndexOf("\n", selStart - 1) + 1;
|
|
401
|
+
const nextLineStart = text.indexOf("\n", selStart) + 1;
|
|
402
|
+
const nextLineEnd = text.indexOf("\n", nextLineStart);
|
|
403
|
+
const finalNextLineEnd = nextLineEnd === -1 ? text.length : nextLineEnd;
|
|
404
|
+
const col = selStart - lineStart;
|
|
405
|
+
const nextLineLength = finalNextLineEnd - nextLineStart;
|
|
406
|
+
selStart = selEnd = nextLineStart + Math.min(col, nextLineLength);
|
|
407
|
+
editContext.updateSelection(selStart, selEnd);
|
|
408
|
+
caret.update(selStart);
|
|
409
|
+
if (onCursorMoveFn) onCursorMoveFn(selStart);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
main.addEventListener("paste", (e) => {
|
|
415
|
+
if (lock) return;
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
const pasteText = e.clipboardData.getData("text/plain");
|
|
418
|
+
text = text.slice(0, selStart) + pasteText + text.slice(selEnd);
|
|
419
|
+
selStart = selEnd = selStart + pasteText.length;
|
|
420
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
421
|
+
editContext.updateSelection(selStart, selEnd);
|
|
422
|
+
saveState();
|
|
423
|
+
render();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
main.addEventListener("focus", () => {
|
|
427
|
+
isFocused = true;
|
|
428
|
+
parent.style.border = `2px solid ${focusColor}`;
|
|
429
|
+
parent.style.boxShadow = "none";
|
|
430
|
+
caret.show();
|
|
431
|
+
caret.update(selStart);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
main.addEventListener("blur", () => {
|
|
435
|
+
isFocused = false;
|
|
436
|
+
parent.style.border = "2px solid #0000";
|
|
437
|
+
if (boxShadow) parent.style.boxShadow = `1px 1px 1px 1px ${shadowColor}`;
|
|
438
|
+
caret.hide();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
main.addEventListener("click", (e) => {
|
|
442
|
+
main.focus();
|
|
443
|
+
const range = document.caretRangeFromPoint(e.clientX, e.clientY);
|
|
444
|
+
if (!range) return;
|
|
445
|
+
|
|
446
|
+
let offset = 0;
|
|
447
|
+
let remaining = 0;
|
|
448
|
+
const walker = document.createTreeWalker(main, NodeFilter.SHOW_TEXT);
|
|
449
|
+
let node;
|
|
450
|
+
while ((node = walker.nextNode())) {
|
|
451
|
+
if (node === range.startContainer) {
|
|
452
|
+
offset = remaining + range.startOffset;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
remaining += node.textContent.length;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
selStart = selEnd = offset;
|
|
459
|
+
editContext.updateSelection(selStart, selEnd);
|
|
460
|
+
caret.update(selStart);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
render();
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
getValue: () => text.replaceAll("\u200B", ""),
|
|
467
|
+
getCursor: () => selStart,
|
|
468
|
+
setCursor: (pos) => {
|
|
469
|
+
selStart = selEnd = Math.max(0, Math.min(pos, text.length));
|
|
470
|
+
editContext.updateSelection(selStart, selEnd);
|
|
471
|
+
caret.update(selStart);
|
|
472
|
+
},
|
|
473
|
+
undo: undo,
|
|
474
|
+
redo: redo,
|
|
475
|
+
setValue: (val) => {
|
|
476
|
+
text = val.replaceAll("\u200B", "");
|
|
477
|
+
selStart = selEnd = text.length;
|
|
478
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
479
|
+
editContext.updateSelection(selStart, selEnd);
|
|
480
|
+
render();
|
|
481
|
+
},
|
|
482
|
+
id: options.id,
|
|
483
|
+
onChange: (fn) => { onChangeFn = fn; },
|
|
484
|
+
isFocused: () => isFocused,
|
|
485
|
+
setLanguage: async (lang) => {
|
|
486
|
+
if (!languages.registeredLanguages.includes(lang)) {
|
|
487
|
+
const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${lang}.js`);
|
|
488
|
+
languages.registerLanguage(lang, mod.default);
|
|
489
|
+
}
|
|
490
|
+
language = lang;
|
|
491
|
+
render();
|
|
492
|
+
},
|
|
493
|
+
delete: () => {
|
|
494
|
+
parent.removeChild(main);
|
|
495
|
+
parent.removeChild(lineCounter);
|
|
496
|
+
caret.destroy();
|
|
497
|
+
document.head.removeChild(themeLink);
|
|
498
|
+
parent.style = "";
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export { createTextEditor }
|
package/index.js
CHANGED
|
@@ -1,22 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import theme from "./theme.js";
|
|
3
|
-
import language from "./langauges.js";
|
|
4
|
-
|
|
1
|
+
import * as t from "./components/textEditor.js";
|
|
5
2
|
const Caret = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
language
|
|
3
|
+
createEditor: t.createTextEditor,
|
|
4
|
+
createTextEditor: t.createTextEditor
|
|
9
5
|
}
|
|
10
|
-
export default Caret;
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
Caret.editor:
|
|
14
|
-
createEditor() -> backbone of caret, handles ui and abstractions
|
|
15
|
-
Caret.theme:
|
|
16
|
-
setTheme() -> changes the current highlight.js theme
|
|
17
|
-
Caret.langauge:
|
|
18
|
-
init() -> initializes default avaible languages
|
|
19
|
-
registerLanguage() -> registers a new languages
|
|
20
|
-
registeredLangauges[List]: has all the langauges registered
|
|
21
|
-
hljs: the highlight.js module
|
|
22
|
-
*/
|
|
7
|
+
export default Caret;
|
package/package.json
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pfmcodes/caret",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "The official code editor engine for lexius",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
7
|
-
"types": "./types/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./types/index.d.ts",
|
|
11
|
-
"default": "./index.js"
|
|
12
|
-
},
|
|
13
|
-
"./editor": "./esm/index.js",
|
|
14
|
-
"./langauges": "./langauges.js",
|
|
15
|
-
"./theme": "./theme.js"
|
|
16
|
-
},
|
|
17
7
|
"repository": {
|
|
18
8
|
"type": "git",
|
|
19
9
|
"url": "git+https://github.com/PFMCODES/lexius-editor.git"
|
|
@@ -37,5 +27,5 @@
|
|
|
37
27
|
"bugs": {
|
|
38
28
|
"url": "https://github.com/PFMCODES/Caret/issues"
|
|
39
29
|
},
|
|
40
|
-
"homepage": "https://
|
|
41
|
-
}
|
|
30
|
+
"homepage": "https://pfmcodes.onrender.com/apps/caret/"
|
|
31
|
+
}
|
package/utilities.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function isMoreThanOneChange(s1, s2) {
|
|
2
|
+
const len1 = s1.length;
|
|
3
|
+
const len2 = s2.length;
|
|
4
|
+
|
|
5
|
+
// 1. If length difference is > 1, they definitely have > 1 change
|
|
6
|
+
if (Math.abs(len1 - len2) > 1) return true;
|
|
7
|
+
|
|
8
|
+
let count = 0;
|
|
9
|
+
let i = 0, j = 0;
|
|
10
|
+
|
|
11
|
+
while (i < len1 && j < len2) {
|
|
12
|
+
if (s1[i] !== s2[j]) {
|
|
13
|
+
count++;
|
|
14
|
+
if (count > 1) return true; // Exit early
|
|
15
|
+
|
|
16
|
+
if (len1 > len2) {
|
|
17
|
+
i++; // Character deleted in s2
|
|
18
|
+
} else if (len1 < len2) {
|
|
19
|
+
j++; // Character added in s2
|
|
20
|
+
} else {
|
|
21
|
+
i++; j++; // Character replaced
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
i++; j++;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check for a trailing character difference at the end
|
|
29
|
+
if (i < len1 || j < len2) count++;
|
|
30
|
+
|
|
31
|
+
return count > 1;
|
|
32
|
+
}
|