@pfmcodes/caret 0.2.9 → 0.3.0
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 +441 -386
- package/components/caret.js +95 -0
- package/components/font.js +16 -0
- package/components/lineCounter.js +91 -0
- package/components/textEditor.js +411 -0
- package/index.js +4 -19
- package/package.json +1 -1
- package/utilities.js +36 -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,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* caret.js - Custom caret for EditContext based editors
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* createCaret {
|
|
7
|
+
* @param {HTMLElement} parent - the editor container
|
|
8
|
+
* @param {HTMLElement} main - the editor div
|
|
9
|
+
* @param {Object} options {
|
|
10
|
+
* @param {string} caretColor - caret color
|
|
11
|
+
* @param {number} caretWidth - caret width in px, default 2
|
|
12
|
+
* }
|
|
13
|
+
* @returns {{ update: (selStart, main) => void, show: () => void, hide: () => void, destroy: () => void }}
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
function createCaret(parent, main, options = {}) {
|
|
17
|
+
const color = options.caretColor || options.focusColor || "#fff";
|
|
18
|
+
const width = options.caretWidth || 2;
|
|
19
|
+
|
|
20
|
+
const caretEl = document.createElement("div");
|
|
21
|
+
caretEl.className = "Caret-caret-" + options.id;
|
|
22
|
+
caretEl.style.position = "absolute";
|
|
23
|
+
caretEl.style.width = `${width}px`;
|
|
24
|
+
caretEl.style.background = color;
|
|
25
|
+
caretEl.style.pointerEvents = "none";
|
|
26
|
+
caretEl.style.zIndex = "10";
|
|
27
|
+
caretEl.style.opacity = "0";
|
|
28
|
+
caretEl.style.borderRadius = "1px";
|
|
29
|
+
caretEl.style.animation = "caret-blink 1s step-end infinite";
|
|
30
|
+
|
|
31
|
+
if (!document.getElementById("caret-blink-style")) {
|
|
32
|
+
const style = document.createElement("style");
|
|
33
|
+
style.id = "caret-blink-style";
|
|
34
|
+
style.textContent = `@keyframes caret-blink { 0%,100%{opacity:1} 50%{opacity:0} }`;
|
|
35
|
+
document.head.appendChild(style);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
parent.appendChild(caretEl);
|
|
39
|
+
|
|
40
|
+
function update(selStart) {
|
|
41
|
+
// walk text nodes to find exact position
|
|
42
|
+
let remaining = selStart;
|
|
43
|
+
let targetNode = null;
|
|
44
|
+
let targetOffset = 0;
|
|
45
|
+
|
|
46
|
+
const walker = document.createTreeWalker(main, NodeFilter.SHOW_TEXT);
|
|
47
|
+
let node;
|
|
48
|
+
while ((node = walker.nextNode())) {
|
|
49
|
+
if (remaining <= node.textContent.length) {
|
|
50
|
+
targetNode = node;
|
|
51
|
+
targetOffset = remaining;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
remaining -= node.textContent.length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!targetNode) {
|
|
58
|
+
caretEl.style.left = "5px";
|
|
59
|
+
caretEl.style.top = "5px";
|
|
60
|
+
caretEl.style.height = "21px";
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const range = document.createRange();
|
|
65
|
+
range.setStart(targetNode, targetOffset);
|
|
66
|
+
range.collapse(true);
|
|
67
|
+
|
|
68
|
+
const rect = range.getBoundingClientRect();
|
|
69
|
+
const parentRect = parent.getBoundingClientRect();
|
|
70
|
+
|
|
71
|
+
if (rect.width === 0 && rect.height === 0) return;
|
|
72
|
+
|
|
73
|
+
caretEl.style.left = rect.left - parentRect.left + parent.scrollLeft + "px";
|
|
74
|
+
caretEl.style.top = rect.top - parentRect.top + parent.scrollTop + "px";
|
|
75
|
+
caretEl.style.height = (rect.height || 21) + "px";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function show() {
|
|
79
|
+
caretEl.style.opacity = "1";
|
|
80
|
+
caretEl.style.animation = "caret-blink 1s step-end infinite";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hide() {
|
|
84
|
+
caretEl.style.opacity = "0";
|
|
85
|
+
caretEl.style.animation = "none";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function destroy() {
|
|
89
|
+
caretEl.remove();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { update, show, hide, destroy };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { createCaret };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loadFont {
|
|
3
|
+
* @param {string} name - name of the font
|
|
4
|
+
* @param {string/url} url - url to external font
|
|
5
|
+
* @returns {void}
|
|
6
|
+
* }
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// load the font dynamically
|
|
10
|
+
async function loadFont(name, url) {
|
|
11
|
+
const font = new FontFace(name, `url("${url}")`);
|
|
12
|
+
await font.load();
|
|
13
|
+
document.fonts.add(font);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { loadFont }
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createLineCounter {
|
|
3
|
+
* @param {HTMLElement} parent - the parent element the line counter goes inside of
|
|
4
|
+
* @param {number} lines - number of lines in the code
|
|
5
|
+
* @param {string/number} id - id for sepration and distinguishtion of multiple instances
|
|
6
|
+
* @param {object} options {
|
|
7
|
+
* @param {string/color} focusColor - border color of the editor when its in use
|
|
8
|
+
* @param {string/color} background - background color of the line counter
|
|
9
|
+
* @param {boolean} dark - if the user has dark theme enabled or not
|
|
10
|
+
* @param {object} theme {
|
|
11
|
+
* @param {object} dark {
|
|
12
|
+
* @param {string/color} background - background color for the dark theme
|
|
13
|
+
* }
|
|
14
|
+
* @param {object} light {
|
|
15
|
+
* @param {string/color} background - background color for the light theme
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* @param {object} font {
|
|
19
|
+
* @param {string} url - external font url or file path
|
|
20
|
+
* @param {string} name - font name, required for both internal and external custom font application
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { loadFont } from "./font.js";
|
|
27
|
+
|
|
28
|
+
async function createLineCounter(parent, lines, id, options) {
|
|
29
|
+
let fontUrl;
|
|
30
|
+
let fontName;
|
|
31
|
+
const font = options.font || {};
|
|
32
|
+
if (!font.url && !font.name) {
|
|
33
|
+
fontName = "monospace";
|
|
34
|
+
parent.style.fontFamily = fontName;
|
|
35
|
+
}
|
|
36
|
+
else if (!font.url && font.name) {
|
|
37
|
+
parent.style.fontFamily = font.name;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
fontUrl = font.url;
|
|
41
|
+
fontName = font.name;
|
|
42
|
+
loadFont(fontName, fontUrl);
|
|
43
|
+
parent.style.fontFamily = fontName;
|
|
44
|
+
}
|
|
45
|
+
const lineCounter = document.createElement("div");
|
|
46
|
+
lineCounter.className = "lineCounter";
|
|
47
|
+
lineCounter.id = `lineCounter-${id}`;
|
|
48
|
+
const dark = options.dark || false;
|
|
49
|
+
const theme = options.theme || {};
|
|
50
|
+
lineCounter.style.height = "auto";
|
|
51
|
+
lineCounter.style.fontSize = "14px";
|
|
52
|
+
lineCounter.style.lineHeight = "1.5";
|
|
53
|
+
lineCounter.style.minWidth = "25px";
|
|
54
|
+
lineCounter.style.top = "0px";
|
|
55
|
+
lineCounter.style.borderRight = options.focusColor || "#fff";
|
|
56
|
+
if (Object.keys(theme).length === 0) {
|
|
57
|
+
lineCounter.style.color = dark ? "#fff" : "#111";
|
|
58
|
+
lineCounter.style.background = dark ? "#1e1e1e" : "#fff";
|
|
59
|
+
parent.style.background = dark ? "#1e1e1e" : "#fff";
|
|
60
|
+
} else {
|
|
61
|
+
lineCounter.style.color = dark ? theme.dark["color.lineCounter"] : theme.light["color.lineCounter"];
|
|
62
|
+
lineCounter.style.background = dark ? theme.dark["background.lineCounter"] : theme.light["background.lineCounter"];
|
|
63
|
+
parent.style.background = dark ? theme.dark["background.lineCounter"] : theme.light["background.lineCounter"];
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < lines; i++) {
|
|
66
|
+
const lineNumber = document.createElement("div");
|
|
67
|
+
lineNumber.className = "line-number";
|
|
68
|
+
lineNumber.id = `line-number-${i}-${id}`;
|
|
69
|
+
lineNumber.innerText = i + 1;
|
|
70
|
+
lineCounter.appendChild(lineNumber);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
parent.appendChild(lineCounter);
|
|
74
|
+
return lineCounter; // 👈 return so you can update it later
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function updateLineCounter(lineCounter, lines) {
|
|
78
|
+
lineCounter.innerHTML = "";
|
|
79
|
+
for (let i = 0; i < lines; i++) {
|
|
80
|
+
const lineNumber = document.createElement("div");
|
|
81
|
+
lineNumber.className = "line-number";
|
|
82
|
+
lineNumber.innerText = i + 1;
|
|
83
|
+
lineCounter.appendChild(lineNumber);
|
|
84
|
+
lineNumber.style.height = "calc(14px * 1.5)"; // font-size * line-height
|
|
85
|
+
lineNumber.style.display = "flex";
|
|
86
|
+
lineNumber.style.alignItems = "center";
|
|
87
|
+
lineNumber.style.margin = "auto";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { createLineCounter, updateLineCounter };
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createTextEditor {
|
|
3
|
+
* @param {string} content - Initial content of the editor
|
|
4
|
+
* @param {HTMLElement} parent - The element to append the editor to
|
|
5
|
+
* @param {Object} options - Optional configuration {
|
|
6
|
+
* @param {boolean} dark - dark theme is enabled or not, false by default
|
|
7
|
+
* @param {boolean} shadow - should boxShadow be enabled, true by default
|
|
8
|
+
* @param {string} focusColor - color of focus border, by default it is purple(#7c3aed)
|
|
9
|
+
* @param {string} shadowColor - only needed if the box shadow is enabled, default is black
|
|
10
|
+
* @param {boolean} lock - if enabled, makes the editor non editable, false by default
|
|
11
|
+
* @param {string} language - language for syntax highlighting, default is plaintext
|
|
12
|
+
* @param {string} hlTheme - highlight.js theme, default is hybrid
|
|
13
|
+
* @param {JSON} font {
|
|
14
|
+
* @param {string} url - optional, only needed if applying custom font
|
|
15
|
+
* @param {string} name - required if you want custom font with or without external font
|
|
16
|
+
* }
|
|
17
|
+
* @param {string/number} id - unique id for the editor for multiple instances
|
|
18
|
+
* @param {JSON} theme {
|
|
19
|
+
* @param {object} dark {
|
|
20
|
+
* @param {string/color} background - background color for the dark theme
|
|
21
|
+
* @param {string/color} color.editor - text color for dark theme
|
|
22
|
+
* @param {string/color} editor.caret - caret color for dark theme
|
|
23
|
+
* }
|
|
24
|
+
* @param {object} light {
|
|
25
|
+
* @param {string/color} background - background color for the light theme
|
|
26
|
+
* @param {string/color} color.editor - text color for light theme
|
|
27
|
+
* @param {string/color} editor.caret - caret color for light theme
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* @returns {object} {
|
|
32
|
+
* @return {string} getValue => () - returns the current value of editor
|
|
33
|
+
* @return {void} setValue => (@param {string} val) - sets the value of editor
|
|
34
|
+
* @return {void} onChange => (@param {function} fn) - fires when value changes
|
|
35
|
+
* @return {boolean} isFocused - tells if the editor is focused
|
|
36
|
+
* @return {void} setLanguage => (@param {string} lang) - changes highlight language
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
*/
|
|
40
|
+
import { createLineCounter, updateLineCounter } from "./lineCounter.js";
|
|
41
|
+
import { loadFont } from "./font.js";
|
|
42
|
+
import { createCaret } from "./caret.js";
|
|
43
|
+
// @ts-ignore
|
|
44
|
+
import hljs from "https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/core.js";
|
|
45
|
+
import languages from "./languages.js";
|
|
46
|
+
|
|
47
|
+
languages.init();
|
|
48
|
+
|
|
49
|
+
async function createTextEditor(parent, content = "", id, options = {}) {
|
|
50
|
+
if (id === undefined || id === null || (typeof id !== "string" && typeof id !== "number")) {
|
|
51
|
+
console.error(`parameter 'id' of function createTextEditor must not be '${typeof id}', it must be a number or string`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!parent || !(parent instanceof HTMLElement)) {
|
|
55
|
+
console.error(`'parent' parameter of function 'createTextEditor' must be an HTMLElement`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!("EditContext" in window)) {
|
|
59
|
+
console.error("EditContext API is not supported in this browser");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// undo/redo stack
|
|
64
|
+
if (!window.caret) window.caret = {};
|
|
65
|
+
window.caret[`undoStack.${id}`] = [{ content, cursor: 0 }];
|
|
66
|
+
window.caret[`redoStack.${id}`] = [];
|
|
67
|
+
|
|
68
|
+
const lock = options.lock || false;
|
|
69
|
+
const focusColor = options.focusColor || '#7c3aed';
|
|
70
|
+
const dark = options.dark || false;
|
|
71
|
+
const boxShadow = options.shadow ?? true;
|
|
72
|
+
const shadowColor = options.shadowColor || "#000";
|
|
73
|
+
const theme = options.theme;
|
|
74
|
+
const font = options.font || {};
|
|
75
|
+
let language = options.language || "plaintext";
|
|
76
|
+
|
|
77
|
+
// load hljs theme
|
|
78
|
+
const themeLink = document.createElement("link");
|
|
79
|
+
themeLink.rel = "stylesheet";
|
|
80
|
+
themeLink.id = `caret-theme-${id}`;
|
|
81
|
+
themeLink.href = options.hlTheme
|
|
82
|
+
? `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/${options.hlTheme}.css`
|
|
83
|
+
: `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/hybrid.css`;
|
|
84
|
+
document.head.appendChild(themeLink);
|
|
85
|
+
|
|
86
|
+
// load language if not registered
|
|
87
|
+
if (!languages.registeredLanguages.includes(language)) {
|
|
88
|
+
const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${language}.js`);
|
|
89
|
+
languages.registerLanguage(language, mod.default);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let fontName;
|
|
93
|
+
if (!font.url && !font.name) {
|
|
94
|
+
fontName = "monospace";
|
|
95
|
+
parent.style.fontFamily = fontName;
|
|
96
|
+
} else if (!font.url && font.name) {
|
|
97
|
+
parent.style.fontFamily = font.name;
|
|
98
|
+
} else {
|
|
99
|
+
fontName = font.name;
|
|
100
|
+
loadFont(fontName, font.url);
|
|
101
|
+
parent.style.fontFamily = fontName;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// text model
|
|
105
|
+
let text = content;
|
|
106
|
+
let selStart = content.length;
|
|
107
|
+
let selEnd = content.length;
|
|
108
|
+
let isFocused = false;
|
|
109
|
+
let onChangeFn = null;
|
|
110
|
+
|
|
111
|
+
// EditContext
|
|
112
|
+
const editContext = new EditContext({
|
|
113
|
+
text,
|
|
114
|
+
selectionStart: selStart,
|
|
115
|
+
selectionEnd: selEnd
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// main div
|
|
119
|
+
const main = document.createElement("div");
|
|
120
|
+
main.editContext = editContext;
|
|
121
|
+
main.tabIndex = 0;
|
|
122
|
+
main.style.whiteSpace = "pre";
|
|
123
|
+
main.style.height = "100%";
|
|
124
|
+
main.style.width = "100%";
|
|
125
|
+
main.style.minWidth = "50px";
|
|
126
|
+
main.style.minHeight = "25px";
|
|
127
|
+
main.style.fontSize = "14px";
|
|
128
|
+
main.style.lineHeight = "1.5";
|
|
129
|
+
main.style.outline = "none";
|
|
130
|
+
main.style.boxSizing = "border-box";
|
|
131
|
+
main.style.borderTopRightRadius = "5px";
|
|
132
|
+
main.style.borderBottomRightRadius = "5px";
|
|
133
|
+
main.style.transition = "all 0.2s ease-in-out";
|
|
134
|
+
main.style.display = "block";
|
|
135
|
+
main.style.paddingTop = "5px";
|
|
136
|
+
main.style.overflowX = "auto";
|
|
137
|
+
main.style.scrollbarWidth = "none";
|
|
138
|
+
main.style.cursor = "text";
|
|
139
|
+
main.style.userSelect = "none";
|
|
140
|
+
main.style.caretColor = "transparent";
|
|
141
|
+
|
|
142
|
+
if (boxShadow) main.style.boxShadow = `1px 1px 1px 1px ${shadowColor}`;
|
|
143
|
+
|
|
144
|
+
if (!theme) {
|
|
145
|
+
main.style.background = dark ? "#101010" : "#d4d4d4";
|
|
146
|
+
main.style.color = dark ? "#fff" : "#000";
|
|
147
|
+
} else {
|
|
148
|
+
main.style.background = dark ? theme.dark["background.editor"] : theme.light["background.editor"];
|
|
149
|
+
main.style.color = dark ? theme.dark["color.editor"] : theme.light["color.editor"];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// caret color
|
|
153
|
+
let caretColor;
|
|
154
|
+
if (options.theme) {
|
|
155
|
+
caretColor = dark ? options.theme.dark["editor.caret"] : options.theme.light["editor.caret"];
|
|
156
|
+
} else {
|
|
157
|
+
caretColor = "#fff";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// parent styles
|
|
161
|
+
parent.style.display = "flex";
|
|
162
|
+
parent.style.alignItems = "flex-start";
|
|
163
|
+
parent.style.border = "2px solid #0000";
|
|
164
|
+
parent.style.padding = "5px";
|
|
165
|
+
parent.style.position = "relative";
|
|
166
|
+
|
|
167
|
+
// line counter
|
|
168
|
+
let lineCounter;
|
|
169
|
+
lineCounter = await createLineCounter(parent, content.split("\n").length, id, options);
|
|
170
|
+
|
|
171
|
+
parent.appendChild(main);
|
|
172
|
+
|
|
173
|
+
// caret
|
|
174
|
+
const caret = createCaret(parent, main, { ...options, caretColor });
|
|
175
|
+
|
|
176
|
+
// --- render ---
|
|
177
|
+
function render() {
|
|
178
|
+
const highlighted = unescapeHTML(hljs.highlight(unescapeHTML(text), { language }).value);
|
|
179
|
+
main.innerHTML = highlighted;
|
|
180
|
+
updateLineCounter(lineCounter, text.trimEnd().split("\n").length);
|
|
181
|
+
caret.update(selStart);
|
|
182
|
+
if (onChangeFn) onChangeFn(text);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- undo/redo ---
|
|
186
|
+
function saveState() {
|
|
187
|
+
const stack = window.caret[`undoStack.${id}`];
|
|
188
|
+
if (text !== stack[stack.length - 1]?.content) {
|
|
189
|
+
stack.push({ content: text, cursor: selStart });
|
|
190
|
+
window.caret[`redoStack.${id}`] = [];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function undo() {
|
|
195
|
+
const stack = window.caret[`undoStack.${id}`];
|
|
196
|
+
const redoStack = window.caret[`redoStack.${id}`];
|
|
197
|
+
if (stack.length <= 1) return;
|
|
198
|
+
const current = stack.pop();
|
|
199
|
+
redoStack.push(current);
|
|
200
|
+
const prev = stack[stack.length - 1];
|
|
201
|
+
text = prev.content;
|
|
202
|
+
const diff = current.content.length - prev.content.length;
|
|
203
|
+
selStart = selEnd = Math.max(0, current.cursor - diff);
|
|
204
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
205
|
+
editContext.updateSelection(selStart, selEnd);
|
|
206
|
+
render();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function redo() {
|
|
210
|
+
const stack = window.caret[`undoStack.${id}`];
|
|
211
|
+
const redoStack = window.caret[`redoStack.${id}`];
|
|
212
|
+
if (redoStack.length === 0) return;
|
|
213
|
+
const next = redoStack.pop();
|
|
214
|
+
stack.push(next);
|
|
215
|
+
text = next.content;
|
|
216
|
+
selStart = selEnd = next.cursor;
|
|
217
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
218
|
+
editContext.updateSelection(selStart, selEnd);
|
|
219
|
+
render();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- EditContext events ---
|
|
223
|
+
editContext.addEventListener("textupdate", (e) => {
|
|
224
|
+
if (lock) return;
|
|
225
|
+
text = text.slice(0, e.updateRangeStart) + e.text + text.slice(e.updateRangeEnd);
|
|
226
|
+
selStart = selEnd = e.selectionStart;
|
|
227
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
228
|
+
saveState();
|
|
229
|
+
render();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
editContext.addEventListener("selectionchange", (e) => {
|
|
233
|
+
selStart = e.selectionStart;
|
|
234
|
+
selEnd = e.selectionEnd;
|
|
235
|
+
caret.update(selStart);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
editContext.addEventListener("textformatupdate", () => {
|
|
239
|
+
// IME formatting — ignore for now
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// --- keyboard events ---
|
|
243
|
+
main.addEventListener("keydown", (e) => {
|
|
244
|
+
if (lock) return;
|
|
245
|
+
|
|
246
|
+
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === "z") {
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
undo();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "y") {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
redo();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "Z") {
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
redo();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Tab
|
|
263
|
+
if (e.key === "Tab" && !e.shiftKey) {
|
|
264
|
+
e.preventDefault();
|
|
265
|
+
const indent = " ";
|
|
266
|
+
if (selStart !== selEnd) {
|
|
267
|
+
const before = text.slice(0, selStart);
|
|
268
|
+
const selected = text.slice(selStart, selEnd);
|
|
269
|
+
const after = text.slice(selEnd);
|
|
270
|
+
const indented = selected.split("\n").map(l => indent + l).join("\n");
|
|
271
|
+
text = before + indented + after;
|
|
272
|
+
selEnd = selStart + indented.length;
|
|
273
|
+
} else {
|
|
274
|
+
text = text.slice(0, selStart) + indent + text.slice(selStart);
|
|
275
|
+
selStart = selEnd = selStart + indent.length;
|
|
276
|
+
}
|
|
277
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
278
|
+
editContext.updateSelection(selStart, selEnd);
|
|
279
|
+
saveState();
|
|
280
|
+
render();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Shift+Tab
|
|
285
|
+
if (e.shiftKey && e.key === "Tab") {
|
|
286
|
+
e.preventDefault();
|
|
287
|
+
if (selStart !== selEnd) {
|
|
288
|
+
const before = text.slice(0, selStart);
|
|
289
|
+
const selected = text.slice(selStart, selEnd);
|
|
290
|
+
const after = text.slice(selEnd);
|
|
291
|
+
const unindented = selected.split("\n").map(l =>
|
|
292
|
+
l.startsWith(" ") ? l.slice(4) :
|
|
293
|
+
l.startsWith("\t") ? l.slice(1) : l
|
|
294
|
+
).join("\n");
|
|
295
|
+
text = before + unindented + after;
|
|
296
|
+
selEnd = selStart + unindented.length;
|
|
297
|
+
} else {
|
|
298
|
+
const lineStart = text.lastIndexOf("\n", selStart - 1) + 1;
|
|
299
|
+
const linePrefix = text.slice(lineStart, lineStart + 4);
|
|
300
|
+
if (linePrefix === " ") {
|
|
301
|
+
text = text.slice(0, lineStart) + text.slice(lineStart + 4);
|
|
302
|
+
selStart = selEnd = Math.max(lineStart, selStart - 4);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
306
|
+
editContext.updateSelection(selStart, selEnd);
|
|
307
|
+
saveState();
|
|
308
|
+
render();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// paste
|
|
314
|
+
main.addEventListener("paste", (e) => {
|
|
315
|
+
if (lock) return;
|
|
316
|
+
e.preventDefault();
|
|
317
|
+
const pasteText = e.clipboardData.getData("text/plain");
|
|
318
|
+
text = text.slice(0, selStart) + pasteText + text.slice(selEnd);
|
|
319
|
+
selStart = selEnd = selStart + pasteText.length;
|
|
320
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
321
|
+
editContext.updateSelection(selStart, selEnd);
|
|
322
|
+
saveState();
|
|
323
|
+
render();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// focus/blur
|
|
327
|
+
main.addEventListener("focus", () => {
|
|
328
|
+
isFocused = true;
|
|
329
|
+
parent.style.border = `2px solid ${focusColor}`;
|
|
330
|
+
parent.style.boxShadow = "none";
|
|
331
|
+
caret.show();
|
|
332
|
+
caret.update(selStart);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
main.addEventListener("blur", () => {
|
|
336
|
+
isFocused = false;
|
|
337
|
+
parent.style.border = "2px solid #0000";
|
|
338
|
+
if (boxShadow) parent.style.boxShadow = `1px 1px 1px 1px ${shadowColor}`;
|
|
339
|
+
caret.hide();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// click to position cursor
|
|
343
|
+
main.addEventListener("click", (e) => {
|
|
344
|
+
main.focus();
|
|
345
|
+
const range = document.caretRangeFromPoint(e.clientX, e.clientY);
|
|
346
|
+
if (!range) return;
|
|
347
|
+
|
|
348
|
+
// walk text nodes to find offset
|
|
349
|
+
let offset = 0;
|
|
350
|
+
let remaining = 0;
|
|
351
|
+
const walker = document.createTreeWalker(main, NodeFilter.SHOW_TEXT);
|
|
352
|
+
let node;
|
|
353
|
+
while ((node = walker.nextNode())) {
|
|
354
|
+
if (node === range.startContainer) {
|
|
355
|
+
offset = remaining + range.startOffset;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
remaining += node.textContent.length;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
selStart = selEnd = offset;
|
|
362
|
+
editContext.updateSelection(selStart, selEnd);
|
|
363
|
+
caret.update(selStart);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// initial render
|
|
367
|
+
render();
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
getValue: () => text,
|
|
371
|
+
setValue: (val) => {
|
|
372
|
+
text = val;
|
|
373
|
+
selStart = selEnd = val.length;
|
|
374
|
+
editContext.updateText(0, editContext.text.length, text);
|
|
375
|
+
editContext.updateSelection(selStart, selEnd);
|
|
376
|
+
render();
|
|
377
|
+
},
|
|
378
|
+
id: options.id,
|
|
379
|
+
onChange: (fn) => { onChangeFn = fn; },
|
|
380
|
+
isFocused: () => isFocused,
|
|
381
|
+
setLanguage: async (lang) => {
|
|
382
|
+
if (!languages.registeredLanguages.includes(lang)) {
|
|
383
|
+
const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${lang}.js`);
|
|
384
|
+
languages.registerLanguage(lang, mod.default);
|
|
385
|
+
}
|
|
386
|
+
language = lang;
|
|
387
|
+
render();
|
|
388
|
+
},
|
|
389
|
+
delete: () => {
|
|
390
|
+
parent.removeChild(main);
|
|
391
|
+
parent.removeChild(lineCounter);
|
|
392
|
+
caret.destroy();
|
|
393
|
+
document.head.removeChild(themeLink);
|
|
394
|
+
parent.style = "";
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function unescapeHTML(str) {
|
|
400
|
+
const entities = {
|
|
401
|
+
'&': '&',
|
|
402
|
+
'<': '<',
|
|
403
|
+
'>': '>',
|
|
404
|
+
'"': '"',
|
|
405
|
+
''': "'",
|
|
406
|
+
''': "'"
|
|
407
|
+
};
|
|
408
|
+
return str.replace(/&|<|>|"|'|'/g, tag => entities[tag] || tag);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
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
package/utilities.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function intoString(a) {
|
|
2
|
+
return `${a}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isMoreThanOneChange(s1, s2) {
|
|
6
|
+
const len1 = s1.length;
|
|
7
|
+
const len2 = s2.length;
|
|
8
|
+
|
|
9
|
+
// 1. If length difference is > 1, they definitely have > 1 change
|
|
10
|
+
if (Math.abs(len1 - len2) > 1) return true;
|
|
11
|
+
|
|
12
|
+
let count = 0;
|
|
13
|
+
let i = 0, j = 0;
|
|
14
|
+
|
|
15
|
+
while (i < len1 && j < len2) {
|
|
16
|
+
if (s1[i] !== s2[j]) {
|
|
17
|
+
count++;
|
|
18
|
+
if (count > 1) return true; // Exit early
|
|
19
|
+
|
|
20
|
+
if (len1 > len2) {
|
|
21
|
+
i++; // Character deleted in s2
|
|
22
|
+
} else if (len1 < len2) {
|
|
23
|
+
j++; // Character added in s2
|
|
24
|
+
} else {
|
|
25
|
+
i++; j++; // Character replaced
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
i++; j++;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check for a trailing character difference at the end
|
|
33
|
+
if (i < len1 || j < len2) count++;
|
|
34
|
+
|
|
35
|
+
return count > 1;
|
|
36
|
+
}
|