forked from WebCoder49/code-input
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcode-input.js
More file actions
332 lines (278 loc) · 14 KB
/
code-input.js
File metadata and controls
332 lines (278 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
// CodeInput
// by WebCoder49
// Based on a CSS-Tricks Post
var codeInput = {
usedTemplates: {
},
defaultTemplate: undefined,
CodeInput: class extends HTMLElement { // Create code input element
constructor() {
super(); // Element
}
/* Syntax-highlighting functions */
update(text) {
if(this.value != text) this.value = text; // Change value attribute if necessary.
if(this.querySelector("textarea").value != text) this.querySelector("textarea").value = text;
let result_element = this.querySelector("pre code");
// Handle final newlines (see article)
if (text[text.length - 1] == "\n") {
text += " ";
}
// Update code
result_element.innerHTML = this.escape_html(text);
// Syntax Highlight
if(this.template.includeCodeInputInHighlightFunc) this.template.highlight(result_element, this);
else this.template.highlight(result_element);
}
sync_scroll() {
/* Scroll result to scroll coords of event - sync with textarea */
let input_element = this.querySelector("textarea");
let result_element = this.template.preElementStyled ? this.querySelector("pre") : this.querySelector("pre code");
// Get and set x and y
result_element.scrollTop = input_element.scrollTop;
result_element.scrollLeft = input_element.scrollLeft;
}
check_tab(event) {
if(event.key != "Tab" || !this.template.isCode) {
return;
}
let input_element = this.querySelector("textarea");
let code = input_element.value;
event.preventDefault(); // stop normal
if(input_element.selectionStart == input_element.selectionEnd) {
let before_selection = code.slice(0, input_element.selectionStart); // text before tab
let after_selection = code.slice(input_element.selectionEnd, input_element.value.length); // text after tab
let cursor_pos = input_element.selectionEnd + 1; // where cursor moves after tab - moving forward by 1 char to after tab
input_element.value = before_selection + "\t" + after_selection; // add tab char
// move cursor
input_element.selectionStart = cursor_pos;
input_element.selectionEnd = cursor_pos;
} else {
let lines = input_element.value.split("\n");
let letter_i = 0;
let selection_start = input_element.selectionStart; // where cursor moves after tab - moving forward by 1 indent
let selection_end = input_element.selectionEnd; // where cursor moves after tab - moving forward by 1 indent
let number_indents = 0;
let first_line_indents = 0;
for (let i = 0; i < lines.length; i++) {
letter_i += lines[i].length;
if(input_element.selectionStart < letter_i && input_element.selectionEnd > letter_i - lines[i].length) {
if(event.shiftKey) {
if(lines[i][0] == "\t") {
lines[i] = lines[i].slice(1);
if(number_indents == 0) first_line_indents--;
number_indents--;
}
} else {
lines[i] = "\t" + lines[i];
if(number_indents == 0) first_line_indents++;
number_indents++;
}
}
}
input_element.value = lines.join("\n");
// move cursor
input_element.selectionStart = selection_start + first_line_indents;
input_element.selectionEnd = selection_end + number_indents;
}
this.update(input_element.value);
}
check_enter(event) {
if(event.key != "Enter" || !this.template.isCode) {
return;
}
event.preventDefault(); // stop normal
let input_element = this.querySelector("textarea");
let lines = input_element.value.split("\n");
let letter_i = 0;
let current_line = lines.length - 1;
let new_line = "";
let number_indents = 0;
// find the index of the line our cursor is currently on
for (let i = 0; i < lines.length; i++) {
letter_i += lines[i].length + 1;
if(input_element.selectionEnd <= letter_i) {
current_line = i;
break;
}
}
// count the number of indents the current line starts with (up to our cursor position in the line)
let cursor_pos_in_line = lines[current_line].length - (letter_i - input_element.selectionEnd) + 1;
for (let i = 0; i < cursor_pos_in_line; i++) {
if (lines[current_line][i] == "\t") {
number_indents++;
} else {
break;
}
}
// determine the text before and after the cursor and chop the current line at the new line break
let text_after_cursor = "";
if (cursor_pos_in_line != lines[current_line].length) {
text_after_cursor = lines[current_line].substring(cursor_pos_in_line);
lines[current_line] = lines[current_line].substring(0, cursor_pos_in_line);
}
// insert our indents and any text from the previous line that might have been after the line break
for (let i = 0; i < number_indents; i++) {
new_line += "\t";
}
new_line += text_after_cursor;
// save the current cursor position
let selection_start = input_element.selectionStart;
let selection_end = input_element.selectionEnd;
// splice our new line into the list of existing lines and join them all back up
lines.splice(current_line + 1, 0, new_line);
input_element.value = lines.join("\n");
// move cursor to new position
input_element.selectionStart = selection_start + number_indents + 1; // count the indent level and the newline character
input_element.selectionEnd = selection_end + number_indents + 1;
this.update(input_element.value);
}
escape_html(text) {
return text.replace(new RegExp("&", "g"), "&").replace(new RegExp("<", "g"), "<"); /* Global RegExp */
}
/* Callbacks */
connectedCallback() {
// Added to document
this.template = codeInput.usedTemplates[this.getAttribute("template") || codeInput.defaultTemplate];
if(this.template.preElementStyled) this.classList.add("code-input_pre-element-styled");
/* Defaults */
let lang = this.getAttribute("lang");
let placeholder = this.getAttribute("placeholder") || this.getAttribute("lang") || "";
let value = this.value || this.innerHTML || "";
this.innerHTML = ""; // Clear Content
/* Create Textarea */
let textarea = document.createElement("textarea");
textarea.placeholder = placeholder;
textarea.value = value;
textarea.setAttribute("spellcheck", "false");
if (this.getAttribute("name")) {
textarea.setAttribute("name", this.getAttribute("name")); // for use in forms
this.removeAttribute("name");
}
textarea.setAttribute("oninput", "this.parentElement.update(this.value); this.parentElement.sync_scroll();");
textarea.setAttribute("onscroll", "this.parentElement.sync_scroll();");
textarea.setAttribute("onkeydown", "this.parentElement.check_tab(event); this.parentElement.check_enter(event);");
this.append(textarea);
/* Create pre code */
let code = document.createElement("code");
if(this.template.isCode && lang != null) code.classList.add("language-" + lang);
code.innerText = value;
let pre = document.createElement("pre");
pre.setAttribute("aria-hidden", "true"); // Hide for screen readers
pre.append(code);
this.append(pre);
/* Add code from value attribute - useful for loading from backend */
this.update(value, this);
}
static get observedAttributes() {
return ["value", "placeholder", "lang", "template"]; // Attributes to monitor
}
attributeChangedCallback(name, oldValue, newValue) {
if(this.isConnected) {
// This will sometimes be called before the element has been created, so trying to update an attribute causes an error.
// Thanks to Kevin Loughead for pointing this out.
switch (name) {
case "value":
// Update code
this.update(newValue);
break;
case "placeholder":
this.querySelector("textarea").placeholder = newValue;
break;
case "template":
this.template = codeInput.usedTemplates[newValue || codeInput.defaultTemplate];
if(this.template.preElementStyled) this.classList.add("code-input_pre-element-styled");
else this.classList.remove("code-input_pre-element-styled");
// Syntax Highlight
this.update(this.value);
case "lang":
let code = this.querySelector("pre code");
let textarea = this.querySelector("textarea");
if(newValue != null) code.className = ("language-" + newValue);
else code.className = "";
if(textarea.placeholder == oldValue) textarea.placeholder = newValue
this.update(this.value);
}
}
}
/* Value attribute */
get value() {
return this.getAttribute("value");
}
set value(val) {
return this.setAttribute("value", val);
}
/* Placeholder attribute */
get placeholder() {
return this.getAttribute("placeholder");
}
set placeholder(val) {
return this.setAttribute("placeholder", val);
}
},
registerTemplate: function(template_name, template) {
// Set default class
codeInput.usedTemplates[template_name] = template;
codeInput.defaultTemplate = template_name;
},
templates: {
custom(highlight=function() {}, preElementStyled=true, isCode=true, includeCodeInputInHighlightFunc=false) {
return {
highlight: highlight,
includeCodeInputInHighlightFunc: includeCodeInputInHighlightFunc,
preElementStyled: preElementStyled,
isCode: isCode,
};
},
prism(prism) { // Dependency: Prism.js (https://prismjs.com/)
return {
includeCodeInputInHighlightFunc: false,
highlight: prism.highlightElement,
preElementStyled: true,
isCode: true
};
},
hljs(hljs) { // Dependency: Highlight.js (https://highlightjs.org/)
return {
includeCodeInputInHighlightFunc: false,
highlight: hljs.highlightElement,
preElementStyled: false,
isCode: true
};
},
characterLimit() {
return {
highlight: function(result_element, code_input) {
let character_limit = Number(code_input.getAttribute("data-character-limit"));
let normal_characters = code_input.escape_html(code_input.value.slice(0, character_limit));
let overflow_characters = code_input.escape_html(code_input.value.slice(character_limit));
result_element.innerHTML = `${normal_characters}<mark class="overflow">${overflow_characters}</mark>`;
if(overflow_characters.length > 0) {
result_element.innerHTML += ` <mark class="overflow-msg">${code_input.getAttribute("data-overflow-msg") || "(Character limit reached)"}</mark>`;
}
},
includeCodeInputInHighlightFunc: true,
preElementStyled: true,
isCode: false
}
},
rainbowText(rainbow_colors=["red", "orangered", "orange", "goldenrod", "gold", "green", "darkgreen", "navy", "blue", "magenta"], delimiter="") {
return {
highlight: function(result_element, code_input) {
let html_result = [];
let sections = code_input.value.split(code_input.template.delimiter);
for (let i = 0; i < sections.length; i++) {
html_result.push(`<span style="color: ${code_input.template.rainbow_colors[i % code_input.template.rainbow_colors.length]}">${code_input.escape_html(sections[i])}</span>`);
}
result_element.innerHTML = html_result.join(code_input.template.delimiter);
},
includeCodeInputInHighlightFunc: true,
preElementStyled: true,
isCode: false,
rainbow_colors: rainbow_colors,
delimiter: delimiter
}
}
}
}
customElements.define("code-input", codeInput.CodeInput); // Set tag