@webcoder49/code-input 1.5.0 → 1.5.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/code-input.js CHANGED
@@ -1,422 +1,759 @@
1
- // CodeInput
2
- // by WebCoder49
3
- // Based on a CSS-Tricks Post
4
-
5
- var codeInput = {
6
- observedAttributes: [ // Doesn't include events, as they are transferred by overriding {add/remove}EventListener
7
- "value",
8
- "name",
9
- "placeholder",
10
- "lang",
11
- "template"
12
- ],
13
- // Attributes to monitor - needs to be global and static
14
-
15
- /* Templates */
16
- usedTemplates: {
17
- },
18
- defaultTemplate: undefined,
19
- templateQueue: {}, // lists of elements for each unrecognised template
20
-
21
- /* Plugins */
22
- plugins: { // Import a plugin from the plugins folder and it will be saved here.
23
- },
24
- Plugin: class {
25
- constructor() {
26
- console.log("code-input: plugin: Created plugin!");
27
-
28
- // Add attributes
29
- codeInput.observedAttributes = codeInput.observedAttributes.concat(self.observedAttributes);
30
- }
31
-
32
- /* Runs before code is highlighted; Params: codeInput element) */
33
- beforeHighlight(codeInput) {}
34
- /* Runs after code is highlighted; Params: codeInput element) */
35
- afterHighlight(codeInput) {}
36
- /* Runs before elements are added into a `code-input`; Params: codeInput element) */
37
- beforeElementsAdded(codeInput) {}
38
- /* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
39
- afterElementsAdded(codeInput) {}
40
- /* Runs when an attribute of a `code-input` is changed (you must add the attribute name to observedAttributes); Params: codeInput element, name attribute name, oldValue previous value of attribute, newValue changed value of attribute) */
41
- attributeChanged(codeInput, name, oldValue, newValue) {}
42
- observedAttributes = []
43
- },
44
-
45
- /* Main */
46
- CodeInput: class extends HTMLElement { // Create code input element
47
- constructor() {
48
- super(); // Element
49
- }
50
-
51
- bound_callbacks = {}; // Callback without this context > Callback with forced codeInput elem this
52
-
53
- /* Run this event in all plugins with a optional list of arguments */
54
- plugin_evt(id, args) {
55
- // Run the event `id` in each plugin
56
- for (let i in this.template.plugins) {
57
- let plugin = this.template.plugins[i];
58
- if (id in plugin) {
59
- if(args === undefined) {
60
- plugin[id](this);
61
- } else {
62
- plugin[id](this, ...args);
63
- }
64
- }
65
- }
66
- }
67
-
68
- /* Syntax-highlighting functions */
69
- update(text) {
70
- // Prevent this from running multiple times on the same input when "value" attribute is changed,
71
- // by not running when value is already equal to the input of this (implying update has already
72
- // been run). Thank you to peterprvy for this.
73
- if(this.ignoreValueUpdate) return;
74
-
75
- this.ignoreValueUpdate = true;
76
- this.value = text; // Change value attribute if necessary.
77
- this.ignoreValueUpdate = false;
78
- if(this.querySelector("textarea").value != text) this.querySelector("textarea").value = text;
79
-
80
-
81
- let result_element = this.querySelector("pre code");
82
-
83
- // Handle final newlines (see article)
84
- if (text[text.length - 1] == "\n") {
85
- text += " ";
86
- }
87
-
88
- // Update code
89
- result_element.innerHTML = this.escape_html(text);
90
- this.plugin_evt("beforeHighlight");
91
-
92
- // Syntax Highlight
93
- if(this.template.includeCodeInputInHighlightFunc) this.template.highlight(result_element, this);
94
- else this.template.highlight(result_element);
95
-
96
- this.plugin_evt("afterHighlight");
97
- }
98
-
99
- sync_scroll() {
100
- /* Scroll result to scroll coords of event - sync with textarea */
101
- let input_element = this.querySelector("textarea");
102
- let result_element = this.template.preElementStyled ? this.querySelector("pre") : this.querySelector("pre code");
103
- // Get and set x and y
104
- result_element.scrollTop = input_element.scrollTop;
105
- result_element.scrollLeft = input_element.scrollLeft;
106
- }
107
-
108
- escape_html(text) {
109
- return text.replace(new RegExp("&", "g"), "&amp;").replace(new RegExp("<", "g"), "&lt;"); /* Global RegExp */
110
- }
111
-
112
- /* Get the template for this element or add to the unrecognised template queue. */
113
- get_template() {
114
- // Get name of template
115
- let template_name;
116
- if(this.getAttribute("template") == undefined) {
117
- // Default
118
- template_name = codeInput.defaultTemplate;
119
- } else {
120
- template_name = this.getAttribute("template");
121
- }
122
- // Get template
123
- if(template_name in codeInput.usedTemplates) {
124
- return codeInput.usedTemplates[template_name];
125
- } else {
126
- // Doesn't exist - add to queue
127
- if( !(template_name in codeInput.templateQueue)) {
128
- codeInput.templateQueue[template_name] = [];
129
- }
130
- codeInput.templateQueue[template_name].push(this);
131
- return undefined;
132
- }
133
- codeInput.usedTemplates[codeInput.defaultTemplate]
134
- }
135
- /* Set up element when a template is added */
136
- setup() {
137
- this.classList.add("code-input_registered"); // Remove register message
138
- if(this.template.preElementStyled) this.classList.add("code-input_pre-element-styled");
139
-
140
- this.plugin_evt("beforeElementsAdded");
141
-
142
- /* Defaults */
143
- let lang = this.getAttribute("lang");
144
- let placeholder = this.getAttribute("placeholder") || this.getAttribute("lang") || "";
145
- let value = this.value || this.innerHTML || "";
146
-
147
- this.innerHTML = ""; // Clear Content
148
-
149
- /* Create Textarea */
150
- let textarea = document.createElement("textarea");
151
- textarea.placeholder = placeholder;
152
- textarea.value = value;
153
- textarea.setAttribute("spellcheck", "false");
154
-
155
- if (this.getAttribute("name")) {
156
- textarea.setAttribute("name", this.getAttribute("name")); // for use in forms
157
- }
158
-
159
- textarea.addEventListener('input',(evt) => { textarea.parentElement.update(textarea.value); textarea.parentElement.sync_scroll();});
160
- textarea.addEventListener('scroll',(evt) => textarea.parentElement.sync_scroll());
161
- this.append(textarea);
162
-
163
- /* Create pre code */
164
- let code = document.createElement("code");
165
- let pre = document.createElement("pre");
166
- pre.setAttribute("aria-hidden", "true"); // Hide for screen readers
167
- pre.append(code);
168
- this.append(pre);
169
-
170
- if(this.template.isCode) {
171
- if(lang != undefined && lang != "") {
172
- code.classList.add("language-" + lang);
173
- }
174
- }
175
-
176
- this.plugin_evt("afterElementsAdded");
177
-
178
- /* Add code from value attribute - useful for loading from backend */
179
- this.update(value, this);
180
- }
181
-
182
- /* Callbacks */
183
- connectedCallback() {
184
- // Added to document
185
- this.template = this.get_template();
186
- if(this.template != undefined) this.setup();
187
- }
188
- static get observedAttributes() {
189
- return codeInput.observedAttributes;
190
- }
191
-
192
- attributeChangedCallback(name, oldValue, newValue) {
193
- if(this.isConnected) {
194
- // This will sometimes be called before the element has been created, so trying to update an attribute causes an error.
195
- // Thanks to Kevin Loughead for pointing this out.
196
-
197
- this.plugin_evt("attributeChanged", [name, oldValue, newValue]); // Plugin event
198
- switch (name) {
199
-
200
- case "value":
201
- this.update(newValue);
202
- break;
203
-
204
- case "name":
205
- if(this.querySelector("textarea") !== null) {
206
- this.querySelector("textarea").setAttribute("name", newValue); // for use in forms
207
- }
208
- break;
209
-
210
- case "placeholder":
211
- this.querySelector("textarea").placeholder = newValue;
212
- break;
213
- case "template":
214
- this.template = codeInput.usedTemplates[newValue || codeInput.defaultTemplate];
215
- if(this.template.preElementStyled) this.classList.add("code-input_pre-element-styled");
216
- else this.classList.remove("code-input_pre-element-styled");
217
- // Syntax Highlight
218
- this.update(this.value);
219
-
220
- break;
221
-
222
- case "lang":
223
-
224
- let code = this.querySelector("pre code");
225
- let main_textarea = this.querySelector("textarea");
226
-
227
- // Check not already updated
228
- if(newValue != null) {
229
- newValue = newValue.toLowerCase();
230
-
231
- if(code.classList.contains(`language-${newValue}`)) break; // Already updated
232
- }
233
-
234
-
235
- // Case insensitive
236
- oldValue = oldValue.toLowerCase();
237
-
238
- // Remove old language class and add new
239
- console.log("code-input: Language: REMOVE", "language-" + oldValue);
240
- code.classList.remove("language-" + oldValue); // From CODE
241
- code.parentElement.classList.remove("language-" + oldValue); // From PRE
242
- code.classList.remove("language-none"); // Prism
243
- code.parentElement.classList.remove("language-none"); // Prism
244
-
245
- if(newValue != undefined && newValue != "") {
246
- code.classList.add("language-" + newValue);
247
- console.log("code-input: Language:ADD", "language-" + newValue);
248
- }
249
-
250
- if(main_textarea.placeholder == oldValue) main_textarea.placeholder = newValue;
251
-
252
- this.update(this.value);
253
-
254
- break;
255
- }
256
- }
257
-
258
- }
259
-
260
- // /* Transfer an event by name from this to an inner element. */
261
- // transfer_event(evt_name, transfer_to, oldValue, newValue) {
262
- // if(oldValue) { // Remove old listener
263
- // transfer_to.removeEventListener(evt_name, this.last_events[evt_name]);
264
- // }
265
- // if(newValue) {
266
- // this.last_events[evt_name] = this[`on${evt_name}`].bind(this);
267
- // transfer_to.addEventListener(evt_name, this.last_events[evt_name]);
268
- // this.removeEventListener(evt_name, newValue);
269
- // }
270
- // }
271
-
272
- /* Override addEventListener so event listener added to necessary child. Returns callback bound to code-input element as `this` */
273
- addEventListener(evt_name, callback, thirdParameter=null) {
274
- let boundCallback = callback.bind(this);
275
- this.bound_callbacks[callback] = boundCallback;
276
- if(evt_name == "change") {
277
- if(thirdParameter === null) {
278
- this.querySelector("textarea").addEventListener("change", boundCallback);
279
- } else {
280
- this.querySelector("textarea").addEventListener("change", boundCallback, thirdParameter);
281
- }
282
- } else if(evt_name == "selectionchange") {
283
- if(thirdParameter === null) {
284
- this.querySelector("textarea").addEventListener("selectionchange", boundCallback);
285
- } else {
286
- this.querySelector("textarea").addEventListener("selectionchange", boundCallback, thirdParameter);
287
- }
288
- }
289
- }
290
-
291
- /* Override removeEventListener so event listener removed from necessary child */
292
- removeEventListener(evt_name, callback, thirdParameter=null) {
293
- let boundCallback = this.bound_callbacks[callback];
294
- if(evt_name == "change") {
295
- if(thirdParameter === null) {
296
- this.querySelector("textarea").removeEventListener("change", boundCallback);
297
- } else {
298
- this.querySelector("textarea").removeEventListener("change", boundCallback, thirdParameter);
299
- }
300
- } else if(evt_name == "selectionchange") {
301
- if(thirdParameter === null) {
302
- this.querySelector("textarea").removeEventListener("selectionchange", boundCallback);
303
- } else {
304
- this.querySelector("textarea").removeEventListener("selectionchange", boundCallback, thirdParameter);
305
- }
306
- }
307
- }
308
-
309
- /* Value attribute */
310
- get value() {
311
- return this.getAttribute("value");
312
- }
313
- set value(val) {
314
- return this.setAttribute("value", val);
315
- }
316
- /* Placeholder attribute */
317
- get placeholder() {
318
- return this.getAttribute("placeholder");
319
- }
320
- set placeholder(val) {
321
- return this.setAttribute("placeholder", val);
322
- }
323
-
324
- pluginData = {}; // For plugins to store element-specific data under their name, e.g. <code-input>.pluginData.specialChars
325
- },
326
-
327
- registerTemplate: function(template_name, template) {
328
- // Set default class
329
- codeInput.usedTemplates[template_name] = template;
330
- // Add elements w/ template from queue
331
- if(template_name in codeInput.templateQueue) {
332
- for(let i in codeInput.templateQueue[template_name]) {
333
- elem = codeInput.templateQueue[template_name][i];
334
- elem.template = template;
335
- elem.setup();
336
- }
337
- console.log(`code-input: template: Added existing elements with template ${template_name}`);
338
- }
339
- if(codeInput.defaultTemplate == undefined) {
340
- codeInput.defaultTemplate = template_name;
341
- // Add elements w/ default template from queue
342
- if(undefined in codeInput.templateQueue) {
343
- for(let i in codeInput.templateQueue[undefined]) {
344
- elem = codeInput.templateQueue[undefined][i];
345
- elem.template = template;
346
- elem.setup();
347
- }
348
- }
349
- console.log(`code-input: template: Set template ${template_name} as default`);
350
- }
351
- console.log(`code-input: template: Created template ${template_name}`);
352
- },
353
- templates: {
354
- custom(highlight=function() {}, preElementStyled=true, isCode=true, includeCodeInputInHighlightFunc=false, plugins=[]) {
355
- return {
356
- highlight: highlight,
357
- includeCodeInputInHighlightFunc: includeCodeInputInHighlightFunc,
358
- preElementStyled: preElementStyled,
359
- isCode: isCode,
360
- plugins: plugins,
361
- };
362
- },
363
- prism(prism, plugins=[]) { // Dependency: Prism.js (https://prismjs.com/)
364
- return {
365
- includeCodeInputInHighlightFunc: false,
366
- highlight: prism.highlightElement,
367
- preElementStyled: true,
368
- isCode: true,
369
- plugins: plugins,
370
- };
371
- },
372
- hljs(hljs, plugins=[]) { // Dependency: Highlight.js (https://highlightjs.org/)
373
- return {
374
- includeCodeInputInHighlightFunc: false,
375
- highlight: hljs.highlightElement,
376
- preElementStyled: false,
377
- isCode: true,
378
- plugins: plugins,
379
- };
380
- },
381
- characterLimit() {
382
- return {
383
- highlight: function(result_element, code_input, plugins=[]) {
384
-
385
- let character_limit = Number(code_input.getAttribute("data-character-limit"));
386
-
387
- let normal_characters = code_input.escape_html(code_input.value.slice(0, character_limit));
388
- let overflow_characters = code_input.escape_html(code_input.value.slice(character_limit));
389
-
390
- result_element.innerHTML = `${normal_characters}<mark class="overflow">${overflow_characters}</mark>`;
391
- if(overflow_characters.length > 0) {
392
- result_element.innerHTML += ` <mark class="overflow-msg">${code_input.getAttribute("data-overflow-msg") || "(Character limit reached)"}</mark>`;
393
- }
394
- },
395
- includeCodeInputInHighlightFunc: true,
396
- preElementStyled: true,
397
- isCode: false,
398
- plugins: plugins,
399
- }
400
- },
401
- rainbowText(rainbow_colors=["red", "orangered", "orange", "goldenrod", "gold", "green", "darkgreen", "navy", "blue", "magenta"], delimiter="", plugins=[]) {
402
- return {
403
- highlight: function(result_element, code_input) {
404
- let html_result = [];
405
- let sections = code_input.value.split(code_input.template.delimiter);
406
- for (let i = 0; i < sections.length; i++) {
407
- 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>`);
408
- }
409
- result_element.innerHTML = html_result.join(code_input.template.delimiter);
410
- },
411
- includeCodeInputInHighlightFunc: true,
412
- preElementStyled: true,
413
- isCode: false,
414
- rainbow_colors: rainbow_colors,
415
- delimiter: delimiter,
416
- plugins: plugins,
417
- }
418
- }
419
- }
420
- }
421
-
422
- customElements.define("code-input", codeInput.CodeInput); // Set tag
1
+ /**
2
+ * **code-input** is a library which lets you create custom HTML `<code-input>`
3
+ * elements that act like `<textarea>` elements but support syntax-highlighted
4
+ * code, implemented using any typical syntax highlighting library. [MIT-Licensed]
5
+ *
6
+ * **<https://github.com/WebCoder49/code-input>**
7
+ */
8
+ var codeInput = {
9
+ /**
10
+ * A list of attributes that will trigger the
11
+ * `codeInput.CodeInput.attributeChangedCallback`
12
+ * when modified in a code-input element. This
13
+ * does not include events, which are handled in
14
+ * `codeInput.CodeInput.addEventListener` and
15
+ * `codeInput.CodeInput.removeEventListener`.
16
+ */
17
+ observedAttributes: [
18
+ "value",
19
+ "placeholder",
20
+ "lang",
21
+ "template"
22
+ ],
23
+
24
+ /**
25
+ * A list of attributes that will be moved to
26
+ * the textarea after they are applied on the
27
+ * code-input element.
28
+ */
29
+ textareaSyncAttributes: [
30
+ "value",
31
+ "name",
32
+ // Form validation - https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation#using_built-in_form_validation
33
+ "required",
34
+ "minlength", "maxlength",
35
+ "min", "max",
36
+ "type",
37
+ "pattern"
38
+ ],
39
+
40
+ /**
41
+ * A list of events whose listeners will be moved to
42
+ * the textarea after they are added to the
43
+ * code-input element.
44
+ */
45
+ textareaSyncEvents: [
46
+ "change",
47
+ "selectionchange",
48
+ "invalid",
49
+ "input"
50
+ ],
51
+
52
+ /* ------------------------------------
53
+ * ------------Templates---------------
54
+ * ------------------------------------ */
55
+
56
+ /**
57
+ * The templates currently available for any code-input elements
58
+ * to use. Registered using `codeInput.registerTemplate`.
59
+ * Key - Template Name
60
+ * Value - A Template Object
61
+ * @type {Object}
62
+ */
63
+ usedTemplates: {
64
+ },
65
+ /**
66
+ * The name of the default template that a code-input element that
67
+ * does not specify the template attribute uses.
68
+ * @type {string}
69
+ */
70
+ defaultTemplate: undefined,
71
+ /**
72
+ * A queue of elements waiting for a template to be registered,
73
+ * allowing elements to be created in HTML with a template before
74
+ * the template is registered in JS, for ease of use.
75
+ * Key - Template Name
76
+ * Value - An array of code-input elements
77
+ * @type {Object}
78
+ */
79
+ templateNotYetRegisteredQueue: {},
80
+
81
+ /**
82
+ * Register a template so code-input elements with a template attribute that equals the templateName will use the template.
83
+ * See `codeInput.templates` for constructors to create templates.
84
+ * @param {string} templateName - the name to register the template under
85
+ * @param {Object} template - a Template object instance - see `codeInput.templates`
86
+ */
87
+ registerTemplate: function (templateName, template) {
88
+ if(!(typeof templateName == "string" || templateName instanceof String)) throw TypeError(`Template for "${templateName}" must be a string.`);
89
+ if(!(typeof template.highlight == "function" || template.highlight instanceof Function)) throw TypeError(`Template for "${templateName}" invalid, because the highlight function provided is not a function; it is "${template.highlight}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`);
90
+ if(!(typeof template.includeCodeInputInHighlightFunc == "boolean" || template.includeCodeInputInHighlightFunc instanceof Boolean)) throw TypeError(`Template for "${templateName}" invalid, because the includeCodeInputInHighlightFunc value provided is not a true or false; it is "${template.includeCodeInputInHighlightFunc}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`);
91
+ if(!(typeof template.preElementStyled == "boolean" || template.preElementStyled instanceof Boolean)) throw TypeError(`Template for "${templateName}" invalid, because the preElementStyled value provided is not a true or false; it is "${template.preElementStyled}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`);
92
+ if(!(typeof template.isCode == "boolean" || template.isCode instanceof Boolean)) throw TypeError(`Template for "${templateName}" invalid, because the isCode value provided is not a true or false; it is "${template.isCode}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`);
93
+ if(!Array.isArray(template.plugins)) throw TypeError(`Template for "${templateName}" invalid, because the plugin array provided is not an array; it is "${template.plugins}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`);
94
+ template.plugins.forEach((plugin, i) => {
95
+ if(!(plugin instanceof codeInput.Plugin)) {
96
+ throw TypeError(`Template for "${templateName}" invalid, because the plugin provided at index ${i} is not valid; it is "${template.plugins[i]}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`);
97
+ }
98
+ });
99
+
100
+ codeInput.usedTemplates[templateName] = template;
101
+ // Add waiting code-input elements wanting this template from queue
102
+ if (templateName in codeInput.templateNotYetRegisteredQueue) {
103
+ for (let i in codeInput.templateNotYetRegisteredQueue[templateName]) {
104
+ elem = codeInput.templateNotYetRegisteredQueue[templateName][i];
105
+ elem.template = template;
106
+ elem.setup();
107
+ }
108
+ console.log(`code-input: template: Added existing elements with template ${templateName}`);
109
+ }
110
+ if (codeInput.defaultTemplate == undefined) {
111
+ codeInput.defaultTemplate = templateName;
112
+ // Add elements with default template from queue
113
+ if (undefined in codeInput.templateNotYetRegisteredQueue) {
114
+ for (let i in codeInput.templateNotYetRegisteredQueue[undefined]) {
115
+ elem = codeInput.templateNotYetRegisteredQueue[undefined][i];
116
+ elem.template = template;
117
+ elem.setup();
118
+ }
119
+ }
120
+ console.log(`code-input: template: Set template ${templateName} as default`);
121
+ }
122
+ console.log(`code-input: template: Created template ${templateName}`);
123
+ },
124
+
125
+ /**
126
+ * Constructors for creating templates.
127
+ * Each code-input element has a template attribute that
128
+ * tells it which template to use.
129
+ * Each template contains functions and preferences that
130
+ * run the syntax-highlighting and let code-input control
131
+ * the highlighting.
132
+ * For adding small pieces of functionality, please see `codeInput.plugins`.
133
+ */
134
+ templates: {
135
+ /**
136
+ * Constructor to create a custom template instance. Pass this into `codeInput.registerTemplate` to use it.
137
+ * I would strongly recommend using the built-in simpler template `codeInput.templates.prism` or `codeInput.templates.hljs`.
138
+ * @param {Function} highlight - a callback to highlight the code, that takes an HTML `<code>` element inside a `<pre>` element as a parameter
139
+ * @param {boolean} preElementStyled - is the <pre> element CSS-styled as well as the `<code>` element? If true, `<pre>` element's scrolling is synchronised; if false, <code> element's scrolling is synchronised.
140
+ * @param {boolean} isCode - is this for writing code? If true, the code-input's lang HTML attribute can be used, and the `<code>` element will be given the class name 'language-[lang attribute's value]'.
141
+ * @param {boolean} includeCodeInputInHighlightFunc - Setting this to true passes the `<code-input>` element as a second argument to the highlight function.
142
+ * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins`
143
+ * @returns template object
144
+ */
145
+ custom(highlight = function () { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, plugins = []) {
146
+ return {
147
+ highlight: highlight,
148
+ includeCodeInputInHighlightFunc: includeCodeInputInHighlightFunc,
149
+ preElementStyled: preElementStyled,
150
+ isCode: isCode,
151
+ plugins: plugins,
152
+ };
153
+ },
154
+ /**
155
+ * Constructor to create a template that uses Prism.js syntax highlighting (https://prismjs.com/)
156
+ * @param {Object} prism Import Prism.js, then after that import pass the `Prism` object as this parameter.
157
+ * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins`
158
+ * @returns template object
159
+ */
160
+ prism(prism, plugins = []) { // Dependency: Prism.js (https://prismjs.com/)
161
+ return {
162
+ includeCodeInputInHighlightFunc: false,
163
+ highlight: prism.highlightElement,
164
+ preElementStyled: true,
165
+ isCode: true,
166
+ plugins: plugins,
167
+ };
168
+ },
169
+ /**
170
+ * Constructor to create a template that uses highlight.js syntax highlighting (https://highlightjs.org/)
171
+ * @param {Object} hljs Import highlight.js, then after that import pass the `hljs` object as this parameter.
172
+ * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins`
173
+ * @returns template object
174
+ */
175
+ hljs(hljs, plugins = []) { // Dependency: Highlight.js (https://highlightjs.org/)
176
+ return {
177
+ includeCodeInputInHighlightFunc: false,
178
+ highlight: hljs.highlightElement,
179
+ preElementStyled: false,
180
+ isCode: true,
181
+ plugins: plugins,
182
+ };
183
+ },
184
+
185
+ /**
186
+ * Constructor to create a proof-of-concept template that gives a message if too many characters are typed.
187
+ * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins`
188
+ * @returns template object
189
+ */
190
+ characterLimit(plugins) {
191
+ return {
192
+ highlight: function (resultElement, codeInput, plugins = []) {
193
+
194
+ let characterLimit = Number(codeInput.getAttribute("data-character-limit"));
195
+
196
+ let normalCharacters = codeInput.escapeHtml(codeInput.value.slice(0, characterLimit));
197
+ let overflowCharacters = codeInput.escapeHtml(codeInput.value.slice(characterLimit));
198
+
199
+ resultElement.innerHTML = `${normalCharacters}<mark class="overflow">${overflowCharacters}</mark>`;
200
+ if (overflowCharacters.length > 0) {
201
+ resultElement.innerHTML += ` <mark class="overflow-msg">${codeInput.getAttribute("data-overflow-msg") || "(Character limit reached)"}</mark>`;
202
+ }
203
+ },
204
+ includeCodeInputInHighlightFunc: true,
205
+ preElementStyled: true,
206
+ isCode: false,
207
+ plugins: plugins,
208
+ }
209
+ },
210
+
211
+ /**
212
+ * Constructor to create a proof-of-concept template that shows text in a repeating series of colors.
213
+ * @param {string[]} rainbowColors - An array of CSS colors, in the order each color will be shown
214
+ * @param {string} delimiter - The character used to split up parts of text where each part is a different colour (e.g. "" = characters, " " = words)
215
+ * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins`
216
+ * @returns template object
217
+ */
218
+ rainbowText(rainbowColors = ["red", "orangered", "orange", "goldenrod", "gold", "green", "darkgreen", "navy", "blue", "magenta"], delimiter = "", plugins = []) {
219
+ return {
220
+ highlight: function (resultElement, codeInput) {
221
+ let htmlResult = [];
222
+ let sections = codeInput.value.split(codeInput.template.delimiter);
223
+ for (let i = 0; i < sections.length; i++) {
224
+ htmlResult.push(`<span style="color: ${codeInput.template.rainbowColors[i % codeInput.template.rainbowColors.length]}">${codeInput.escapeHtml(sections[i])}</span>`);
225
+ }
226
+ resultElement.innerHTML = htmlResult.join(codeInput.template.delimiter);
227
+ },
228
+ includeCodeInputInHighlightFunc: true,
229
+ preElementStyled: true,
230
+ isCode: false,
231
+ rainbowColors: rainbowColors,
232
+ delimiter: delimiter,
233
+ plugins: plugins,
234
+ }
235
+ },
236
+
237
+ /**
238
+ * @deprecated Please use `codeInput.characterLimit(plugins)`
239
+ */
240
+ character_limit() {
241
+ return this.characterLimit([]);
242
+ },
243
+ /**
244
+ * @deprecated Please use `codeInput.rainbowText`
245
+ */
246
+ rainbow_text(rainbowColors = ["red", "orangered", "orange", "goldenrod", "gold", "green", "darkgreen", "navy", "blue", "magenta"], delimiter = "", plugins = []) {
247
+ return this.rainbowText(rainbowColors, delimiter, plugins);
248
+ }
249
+ },
250
+
251
+ /* ------------------------------------
252
+ * ------------Plugins-----------------
253
+ * ------------------------------------ */
254
+
255
+ /**
256
+ * Where plugins are stored, after they are imported. The plugin
257
+ * file assigns them a space in this object.
258
+ * For adding completely new syntax-highlighting algorithms, please see `codeInput.templates`.
259
+ * Key - plugin name
260
+ * Value - plugin object
261
+ * @type {Object}
262
+ */
263
+ plugins: {
264
+ },
265
+
266
+ /**
267
+ * Plugins are imported from the plugins folder. They will then
268
+ * provide custom extra functionality to code-input elements.
269
+ */
270
+ Plugin: class {
271
+ constructor() {
272
+ console.log("code-input: plugin: Created plugin");
273
+
274
+ codeInput.observedAttributes = codeInput.observedAttributes.concat(self.observedAttributes);
275
+ }
276
+
277
+ /**
278
+ * Runs before code is highlighted.
279
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
280
+ */
281
+ beforeHighlight(codeInput) { }
282
+ /**
283
+ * Runs after code is highlighted.
284
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
285
+ */
286
+ afterHighlight(codeInput) { }
287
+ /**
288
+ * Runs before elements are added into a code-input element.
289
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
290
+ */
291
+ beforeElementsAdded(codeInput) { }
292
+ /**
293
+ * Runs after elements are added into a code-input element (useful for adding events to the textarea).
294
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
295
+ */
296
+ afterElementsAdded(codeInput) { }
297
+ /**
298
+ * Runs when an attribute of a code-input element is changed (you must add the attribute name to `codeInput.Plugin.observedAttributes` first).
299
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
300
+ * @param {string} name - The name of the attribute
301
+ * @param {string} oldValue - The value of the attribute before it was changed
302
+ * @param {string} newValue - The value of the attribute after it is changed
303
+ */
304
+ attributeChanged(codeInput, name, oldValue, newValue) { }
305
+ /**
306
+ * The HTML attributes to watch for this plugin, and report any
307
+ * modifications to the `codeInput.Plugin.attributeChanged` method.
308
+ */
309
+ observedAttributes = []
310
+ },
311
+
312
+ /* ------------------------------------
313
+ * -------------Main-------------------
314
+ * ------------------------------------ */
315
+
316
+ /**
317
+ * A code-input element.
318
+ */
319
+ CodeInput: class extends HTMLElement {
320
+ constructor() {
321
+ super(); // Element
322
+ }
323
+
324
+ /**
325
+ * When events are transferred to the textarea element, callbacks
326
+ * are bound to set the this variable to the code-inpute element
327
+ * rather than the textarea. This allows the callback to be converted
328
+ * to a bound one:
329
+ * Key - Callback not bound
330
+ * Value - Callback that is bound, with this equalling the code-input element in the callback
331
+ */
332
+ boundEventCallbacks = {};
333
+
334
+ /** Trigger this event in all plugins with a optional list of arguments
335
+ * @param {string} eventName - the name of the event to trigger
336
+ * @param {Array} args - the arguments to pass into the event callback in the template after the code-input element. Normally left empty
337
+ */
338
+ pluginEvt(eventName, args) {
339
+ for (let i in this.template.plugins) {
340
+ let plugin = this.template.plugins[i];
341
+ if (eventName in plugin) {
342
+ if (args === undefined) {
343
+ plugin[eventName](this);
344
+ } else {
345
+ plugin[eventName](this, ...args);
346
+ }
347
+ }
348
+ }
349
+ }
350
+
351
+ /* ------------------------------------
352
+ * ----------Main Functionality--------
353
+ * ------------------------------------
354
+ * The main function of a code-input element is to take
355
+ * code written in its textarea element, copy this code into
356
+ * the result (pre code) element, then use the template object
357
+ * to syntax-highlight it. */
358
+
359
+ /** Update the text value to the result element, after the textarea contents have changed.
360
+ * @param {string} value - The text value of the code-input element
361
+ */
362
+ update(value) {
363
+ // Prevent this from running multiple times on the same input when "value" attribute is changed,
364
+ // by not running when value is already equal to the input of this (implying update has already
365
+ // been run). Thank you to peterprvy for this.
366
+ if (this.ignoreValueUpdate) return;
367
+
368
+ this.ignoreValueUpdate = true;
369
+ this.value = value;
370
+ this.ignoreValueUpdate = false;
371
+ if (this.querySelector("textarea").value != value) this.querySelector("textarea").value = value;
372
+
373
+
374
+ let resultElement = this.querySelector("pre code");
375
+
376
+ // Handle final newlines
377
+ if (value[value.length - 1] == "\n") {
378
+ value += " ";
379
+ }
380
+
381
+ // Update code
382
+ resultElement.innerHTML = this.escapeHtml(value);
383
+ this.pluginEvt("beforeHighlight");
384
+
385
+ // Syntax Highlight
386
+ if (this.template.includeCodeInputInHighlightFunc) this.template.highlight(resultElement, this);
387
+ else this.template.highlight(resultElement);
388
+
389
+ this.pluginEvt("afterHighlight");
390
+ }
391
+
392
+ /**
393
+ * Synchronise the scrolling of the textarea to the result element.
394
+ */
395
+ syncScroll() {
396
+ let inputElement = this.querySelector("textarea");
397
+ let resultElement = this.template.preElementStyled ? this.querySelector("pre") : this.querySelector("pre code");
398
+
399
+ resultElement.scrollTop = inputElement.scrollTop;
400
+ resultElement.scrollLeft = inputElement.scrollLeft;
401
+ }
402
+
403
+ /**
404
+ * HTML-escape an arbitrary string.
405
+ * @param {string} text - The original, unescaped text
406
+ * @returns {string} - The new, HTML-escaped text
407
+ */
408
+ escapeHtml(text) {
409
+ return text.replace(new RegExp("&", "g"), "&amp;").replace(new RegExp("<", "g"), "&lt;"); /* Global RegExp */
410
+ }
411
+
412
+ /**
413
+ * Get the template object this code-input element is using.
414
+ * @returns {Object} - Template object
415
+ */
416
+ getTemplate() {
417
+ let templateName;
418
+ if (this.getAttribute("template") == undefined) {
419
+ // Default
420
+ templateName = codeInput.defaultTemplate;
421
+ } else {
422
+ templateName = this.getAttribute("template");
423
+ }
424
+ if (templateName in codeInput.usedTemplates) {
425
+ return codeInput.usedTemplates[templateName];
426
+ } else {
427
+ // Doesn't exist - add to queue
428
+ if (!(templateName in codeInput.templateNotYetRegisteredQueue)) {
429
+ codeInput.templateNotYetRegisteredQueue[templateName] = [];
430
+ }
431
+ codeInput.templateNotYetRegisteredQueue[templateName].push(this);
432
+ return undefined;
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Set up and initialise the textarea.
438
+ * This will be called once the template has been added.
439
+ */
440
+ setup() {
441
+ this.classList.add("code-input_registered"); // Remove register message
442
+ if (this.template.preElementStyled) this.classList.add("code-input_pre-element-styled");
443
+
444
+ this.pluginEvt("beforeElementsAdded");
445
+
446
+ // First-time attribute sync
447
+ let lang = this.getAttribute("lang");
448
+ let placeholder = this.getAttribute("placeholder") || this.getAttribute("lang") || "";
449
+ let value = this.value || this.innerHTML || "";
450
+
451
+ this.innerHTML = ""; // Clear Content
452
+
453
+ // Create textarea
454
+ let textarea = document.createElement("textarea");
455
+ textarea.placeholder = placeholder;
456
+ textarea.value = value;
457
+ textarea.setAttribute("spellcheck", "false");
458
+
459
+ // Synchronise attributes to textarea
460
+ codeInput.textareaSyncAttributes.forEach((attribute) => {
461
+ if (this.hasAttribute(attribute)) {
462
+ textarea.setAttribute(attribute, this.getAttribute(attribute));
463
+ }
464
+ });
465
+
466
+ textarea.addEventListener('input', (evt) => { textarea.parentElement.update(textarea.value); textarea.parentElement.sync_scroll(); });
467
+ textarea.addEventListener('scroll', (evt) => textarea.parentElement.sync_scroll());
468
+
469
+ this.append(textarea);
470
+
471
+ // Create result element
472
+ let code = document.createElement("code");
473
+ let pre = document.createElement("pre");
474
+ pre.setAttribute("aria-hidden", "true"); // Hide for screen readers
475
+ pre.append(code);
476
+ this.append(pre);
477
+
478
+ if (this.template.isCode) {
479
+ if (lang != undefined && lang != "") {
480
+ code.classList.add("language-" + lang);
481
+ }
482
+ }
483
+
484
+ this.pluginEvt("afterElementsAdded");
485
+
486
+ this.update(value);
487
+ }
488
+
489
+ /**
490
+ * @deprecated Please use `codeInput.CodeInput.syncScroll`
491
+ */
492
+ sync_scroll() {
493
+ this.syncScroll();
494
+ }
495
+
496
+ /**
497
+ * @deprecated Please use `codeInput.CodeInput.escapeHtml`
498
+ */
499
+ escape_html(text) {
500
+ return this.escapeHtml(text);
501
+ }
502
+
503
+ /**
504
+ * @deprecated Please use `codeInput.CodeInput.escapeHtml`
505
+ */
506
+ get_template() {
507
+ return this.getTemplate();
508
+ }
509
+
510
+
511
+ /* ------------------------------------
512
+ * -----------Callbacks----------------
513
+ * ------------------------------------
514
+ * Implement the `HTMLElement` callbacks
515
+ * to trigger the main functionality properly. */
516
+
517
+ /**
518
+ * When the code-input element has been added to the document,
519
+ * find its template and set up the element.
520
+ */
521
+ connectedCallback() {
522
+ this.template = this.getTemplate();
523
+ if (this.template != undefined) this.setup();
524
+ }
525
+
526
+ /**
527
+ * Get the HTML attributes that need to be monitored and reported
528
+ * to `codeInput.CodeInput.attributeChangedCallback` when modified.
529
+ */
530
+ static get observedAttributes() {
531
+ return codeInput.observedAttributes.concat(codeInput.textareaSyncAttributes);
532
+ }
533
+
534
+ /**
535
+ * Triggered when an HTML attribute in `codeInput.CodeInput.observedAttributes`
536
+ * has been modified.
537
+ * @param {string} name - The name of the attribute
538
+ * @param {string} oldValue - The value of the attribute before it was changed
539
+ * @param {string} newValue - The value of the attribute after it is changed
540
+ */
541
+ attributeChangedCallback(name, oldValue, newValue) {
542
+ if (this.isConnected) {
543
+ this.pluginEvt("attributeChanged", [name, oldValue, newValue]);
544
+ switch (name) {
545
+
546
+ case "value":
547
+ this.update(newValue);
548
+ break;
549
+ case "placeholder":
550
+ this.querySelector("textarea").placeholder = newValue;
551
+ break;
552
+ case "template":
553
+ this.template = codeInput.usedTemplates[newValue || codeInput.defaultTemplate];
554
+ if (this.template.preElementStyled) this.classList.add("code-input_pre-element-styled");
555
+ else this.classList.remove("code-input_pre-element-styled");
556
+ // Syntax Highlight
557
+ this.update(this.value);
558
+
559
+ break;
560
+
561
+ case "lang":
562
+
563
+ let code = this.querySelector("pre code");
564
+ let mainTextarea = this.querySelector("textarea");
565
+
566
+ // Check not already updated
567
+ if (newValue != null) {
568
+ newValue = newValue.toLowerCase();
569
+
570
+ if (code.classList.contains(`language-${newValue}`)) break; // Already updated
571
+ }
572
+
573
+
574
+ // Case insensitive
575
+ oldValue = oldValue.toLowerCase();
576
+
577
+ // Remove old language class and add new
578
+ console.log("code-input: Language: REMOVE", "language-" + oldValue);
579
+ code.classList.remove("language-" + oldValue); // From CODE
580
+ code.parentElement.classList.remove("language-" + oldValue); // From PRE
581
+ code.classList.remove("language-none"); // Prism
582
+ code.parentElement.classList.remove("language-none"); // Prism
583
+
584
+ if (newValue != undefined && newValue != "") {
585
+ code.classList.add("language-" + newValue);
586
+ console.log("code-input: Language:ADD", "language-" + newValue);
587
+ }
588
+
589
+ if (mainTextarea.placeholder == oldValue) mainTextarea.placeholder = newValue;
590
+
591
+ this.update(this.value);
592
+
593
+ break;
594
+ default:
595
+ if (codeInput.textareaSyncAttributes.includes(name)) {
596
+ this.querySelector("textarea").setAttribute(name, newValue);
597
+ }
598
+ break;
599
+ }
600
+ }
601
+
602
+ }
603
+
604
+ /* ------------------------------------
605
+ * -----------Overrides----------------
606
+ * ------------------------------------
607
+ * Override/Implement ordinary HTML textarea functionality so that the <code-input>
608
+ * element acts just like a <textarea>. */
609
+
610
+ /**
611
+ * @override
612
+ */
613
+ addEventListener(type, listener, options = undefined) {
614
+ let boundCallback = listener.bind(this);
615
+ this.boundEventCallbacks[listener] = boundCallback;
616
+
617
+ if (codeInput.textareaSyncEvents.includes(type)) {
618
+ if (options === undefined) {
619
+ this.querySelector("textarea").addEventListener(type, boundCallback);
620
+ } else {
621
+ this.querySelector("textarea").addEventListener(type, boundCallback, options);
622
+ }
623
+ } else {
624
+ if (options === undefined) {
625
+ super.addEventListener(type, boundCallback);
626
+ } else {
627
+ super.addEventListener(type, boundCallback, options);
628
+ }
629
+ }
630
+ }
631
+
632
+ /**
633
+ * @override
634
+ */
635
+ removeEventListener(type, listener, options = null) {
636
+ let boundCallback = this.boundEventCallbacks[listener];
637
+ if (type == "change") {
638
+ if (options === null) {
639
+ this.querySelector("textarea").removeEventListener("change", boundCallback);
640
+ } else {
641
+ this.querySelector("textarea").removeEventListener("change", boundCallback, options);
642
+ }
643
+ } else if (type == "selectionchange") {
644
+ if (options === null) {
645
+ this.querySelector("textarea").removeEventListener("selectionchange", boundCallback);
646
+ } else {
647
+ this.querySelector("textarea").removeEventListener("selectionchange", boundCallback, options);
648
+ }
649
+ } else {
650
+ super.removeEventListener(type, listener, options);
651
+ }
652
+ }
653
+
654
+ /**
655
+ * Get the text contents of the code-input element.
656
+ */
657
+ get value() {
658
+ return this.getAttribute("value");
659
+ }
660
+ /**
661
+ * Set the text contents of the code-input element.
662
+ * @param {string} val - New text contents
663
+ */
664
+ set value(val) {
665
+ return this.setAttribute("value", val);
666
+ }
667
+
668
+ /**
669
+ * Get the placeholder of the code-input element that appears
670
+ * when no code has been entered.
671
+ */
672
+ get placeholder() {
673
+ return this.getAttribute("placeholder");
674
+ }
675
+ /**
676
+ * Set the placeholder of the code-input element that appears
677
+ * when no code has been entered.
678
+ * @param {string} val - New placeholder
679
+ */
680
+ set placeholder(val) {
681
+ return this.setAttribute("placeholder", val);
682
+ }
683
+
684
+ /**
685
+ * Returns a ValidityState object that represents the validity states of an element.
686
+ *
687
+ * See `HTMLTextAreaElement.validity`
688
+ */
689
+ get validity() {
690
+ return this.querySelector("textarea").validity;
691
+ }
692
+
693
+ /**
694
+ * Returns the error message that would be displayed if the user submits the form, or an empty string if no error message.
695
+ * It also triggers the standard error message, such as "this is a required field". The result is that the user sees validation
696
+ * messages without actually submitting.
697
+ *
698
+ * See `HTMLTextAreaElement.validationMessage`
699
+ */
700
+ get validationMessage() {
701
+ return this.querySelector("textarea").validationMessage;
702
+ }
703
+
704
+ /**
705
+ * Sets a custom error message that is displayed when a form is submitted.
706
+ *
707
+ * See `HTMLTextAreaElement.setCustomValidity`
708
+ * @param error Sets a custom error message that is displayed when a form is submitted.
709
+ */
710
+ setCustomValidity(error) {
711
+ return this.querySelector("textarea").setCustomValidity(error);
712
+ }
713
+
714
+ /**
715
+ * Returns whether a form will validate when it is submitted,
716
+ * without having to submit it.
717
+ *
718
+ * See `HTMLTextAreaElement.checkValidity`
719
+ */
720
+ checkValidity() {
721
+ return this.querySelector("textarea").checkValidity();
722
+ }
723
+
724
+ /**
725
+ * See `HTMLTextAreaElement.reportValidity`
726
+ */
727
+ reportValidity() {
728
+ return this.querySelector("textarea").reportValidity();
729
+ }
730
+
731
+
732
+ /**
733
+ * @override
734
+ */
735
+ setAttribute(qualifiedName, value) {
736
+ super.setAttribute(qualifiedName, value); // code-input
737
+ this.querySelector("textarea").setAttribute(qualifiedName, value); // textarea
738
+ }
739
+
740
+ /**
741
+ * @override
742
+ */
743
+ getAttribute(qualifiedName) {
744
+ if (this.querySelector("textarea") == null) {
745
+ return super.getAttribute(qualifiedName);
746
+ }
747
+ return this.querySelector("textarea").getAttribute(qualifiedName); // textarea
748
+ }
749
+
750
+ /**
751
+ * Allows plugins to store data in the scope of a single element.
752
+ * Key - name of the plugin
753
+ * Value - object of data to be stored; different plugins may use this differently.
754
+ */
755
+ pluginData = {};
756
+ }
757
+ }
758
+
759
+ customElements.define("code-input", codeInput.CodeInput);