@webcoder49/code-input 2.6.0 → 2.6.3

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.
@@ -1,2 +1,1060 @@
1
1
  // NOTICE: This code is @generated from code outside the esm directory. Please do not edit it to contribute!
2
2
 
3
+ /**
4
+ * **code-input** is a library which lets you create custom HTML `<code-input>`
5
+ * elements that act like `<textarea>` elements but support syntax-highlighted
6
+ * code, implemented using any typical syntax highlighting library.
7
+ *
8
+ * License of whole library for bundlers:
9
+ *
10
+ * Copyright 2021-2025 Oliver Geer and contributors
11
+ * @license MIT
12
+ *
13
+ * **<https://code-input-js.org>**
14
+ */
15
+ "use strict";
16
+
17
+
18
+ var codeInput = {
19
+ /**
20
+ * A list of attributes that will trigger functionality in the
21
+ * `codeInput.CodeInput.attributeChangedCallback`
22
+ * when modified in a code-input element. This
23
+ * does not include events, which are handled in
24
+ * `codeInput.CodeInput.addEventListener` and
25
+ * `codeInput.CodeInput.removeEventListener`.
26
+ *
27
+ * This does not include those listed in `codeInput.textareaSyncAttributes` that only synchronise with the textarea element.
28
+ */
29
+ observedAttributes: [
30
+ "value",
31
+ "placeholder",
32
+ "language",
33
+ "lang",
34
+ "template"
35
+ ],
36
+
37
+ /**
38
+ * A list of attributes that will be moved to
39
+ * the textarea after they are applied on the
40
+ * code-input element.
41
+ */
42
+ textareaSyncAttributes: [
43
+ "value",
44
+ // Form validation - https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation#using_built-in_form_validation
45
+ "min", "max",
46
+ "type",
47
+ "pattern",
48
+
49
+ // Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea
50
+ "autocomplete",
51
+ "autocorrect",
52
+ "autofocus",
53
+ "cols",
54
+ "dirname",
55
+ "disabled",
56
+ "form",
57
+ "maxlength",
58
+ "minlength",
59
+ "name",
60
+ "placeholder",
61
+ "readonly",
62
+ "required",
63
+ "rows",
64
+ "spellcheck",
65
+ "wrap"
66
+ ],
67
+
68
+ /**
69
+ * A list of events whose listeners will be moved to
70
+ * the textarea after they are added to the
71
+ * code-input element.
72
+ */
73
+ textareaSyncEvents: [
74
+ "change",
75
+ "selectionchange",
76
+ "invalid",
77
+ "input",
78
+ "focus",
79
+ "blur",
80
+ "focusin",
81
+ "focusout"
82
+ ],
83
+
84
+ /* ------------------------------------
85
+ * ------------Templates---------------
86
+ * ------------------------------------ */
87
+
88
+ /**
89
+ * The templates currently available for any code-input elements
90
+ * to use. Registered using `codeInput.registerTemplate`.
91
+ * Key - Template Name
92
+ * Value - A Template Object
93
+ * @type {Object}
94
+ */
95
+ usedTemplates: {
96
+ },
97
+ /**
98
+ * The name of the default template that a code-input element that
99
+ * does not specify the template attribute uses.
100
+ * @type {string}
101
+ */
102
+ defaultTemplate: undefined,
103
+ /**
104
+ * A queue of elements waiting for a template to be registered,
105
+ * allowing elements to be created in HTML with a template before
106
+ * the template is registered in JS, for ease of use.
107
+ * Key - Template Name
108
+ * Value - An array of code-input elements
109
+ * @type {Object}
110
+ */
111
+ templateNotYetRegisteredQueue: {},
112
+
113
+ /**
114
+ * Register a template so code-input elements with a template attribute that equals the templateName will use the template.
115
+ * See `codeInput.templates` for constructors to create templates.
116
+ * @param {string} templateName - the name to register the template under
117
+ * @param {Object} template - a Template object instance - see `codeInput.templates`
118
+ */
119
+ registerTemplate: function (templateName, template) {
120
+ if(!(typeof templateName == "string" || templateName instanceof String)) throw TypeError(`code-input: Name of template "${templateName}" must be a string.`);
121
+ if(!(typeof template.highlight == "function" || template.highlight instanceof Function)) throw TypeError(`code-input: 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.`);
122
+ if(!(typeof template.includeCodeInputInHighlightFunc == "boolean" || template.includeCodeInputInHighlightFunc instanceof Boolean)) throw TypeError(`code-input: 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.`);
123
+ if(!(typeof template.preElementStyled == "boolean" || template.preElementStyled instanceof Boolean)) throw TypeError(`code-input: 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.`);
124
+ if(!(typeof template.isCode == "boolean" || template.isCode instanceof Boolean)) throw TypeError(`code-input: 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.`);
125
+ if(!Array.isArray(template.plugins)) throw TypeError(`code-input: 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.`);
126
+
127
+ template.plugins.forEach((plugin, i) => {
128
+ if(!(plugin instanceof codeInput.Plugin)) {
129
+ throw TypeError(`code-input: 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.`);
130
+ }
131
+ });
132
+
133
+
134
+ codeInput.usedTemplates[templateName] = template;
135
+ // Add waiting code-input elements wanting this template from queue
136
+ if (templateName in codeInput.templateNotYetRegisteredQueue) {
137
+ for (let i in codeInput.templateNotYetRegisteredQueue[templateName]) {
138
+ const elem = codeInput.templateNotYetRegisteredQueue[templateName][i];
139
+ elem.templateObject = template;
140
+ elem.setup();
141
+ }
142
+ }
143
+
144
+ if (codeInput.defaultTemplate == undefined) {
145
+ codeInput.defaultTemplate = templateName;
146
+ // Add elements with default template from queue
147
+ if (undefined in codeInput.templateNotYetRegisteredQueue) {
148
+ for (let i in codeInput.templateNotYetRegisteredQueue[undefined]) {
149
+ const elem = codeInput.templateNotYetRegisteredQueue[undefined][i];
150
+ elem.templateObject = template;
151
+ elem.setup();
152
+ }
153
+ }
154
+ }
155
+ },
156
+
157
+ /**
158
+ * Please see `codeInput.templates.prism` or `codeInput.templates.hljs`.
159
+ * Templates are used in `<code-input>` elements and once registered with
160
+ * `codeInput.registerTemplate` will be in charge of the highlighting
161
+ * algorithm and settings for all code-inputs with a `template` attribute
162
+ * matching the registered name.
163
+ */
164
+ Template: class {
165
+ /**
166
+ * Constructor to create a custom template instance. Pass this into `codeInput.registerTemplate` to use it.
167
+ * I would strongly recommend using the built-in simpler template `codeInput.templates.prism` or `codeInput.templates.hljs`.
168
+ * @param {(codeElement: HTMLCodeElement, codeInput?: codeInput.CodeInput) => void} highlight - a callback to highlight the code, that takes an HTML `<code>` element inside a `<pre>` element as a parameter
169
+ * @param {boolean} preElementStyled - is the `<pre>` element CSS-styled (if so set to true), or the `<code>` element (false)?
170
+ * @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]'.
171
+ * @param {boolean} includeCodeInputInHighlightFunc - Setting this to true passes the `<code-input>` element as a second argument to the highlight function.
172
+ * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.Plugin`
173
+ * @returns {codeInput.Template} template object
174
+ */
175
+ constructor(highlight = function (codeElement) { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, plugins = []) {
176
+ this.highlight = highlight;
177
+ this.preElementStyled = preElementStyled;
178
+ this.isCode = isCode;
179
+ this.includeCodeInputInHighlightFunc = includeCodeInputInHighlightFunc;
180
+ this.plugins = plugins;
181
+ }
182
+
183
+ /**
184
+ * A callback to highlight the code, that takes an HTML `<code>` element
185
+ * inside a `<pre>` element as a parameter, and an optional second
186
+ * `<code-input>` element parameter if `this.includeCodeInputInHighlightFunc` is
187
+ * `true`.
188
+ */
189
+ highlight = function(codeElement) {};
190
+
191
+ /**
192
+ * Is the <pre> element CSS-styled as well as the `<code>` element?
193
+ * If `true`, `<pre>` element's scrolling is synchronised; if false,
194
+ * <code> element's scrolling is synchronised.
195
+ */
196
+ preElementStyled = true;
197
+
198
+ /**
199
+ * Is this for writing code?
200
+ * If true, the code-input's lang HTML attribute can be used,
201
+ * and the `<code>` element will be given the class name
202
+ * 'language-[lang attribute's value]'.
203
+ */
204
+ isCode = true;
205
+
206
+ /**
207
+ * Setting this to true passes the `<code-input>` element as a
208
+ * second argument to the highlight function.
209
+ */
210
+ includeCodeInputInHighlightFunc = false;
211
+
212
+ /**
213
+ * An array of plugin objects to add extra features -
214
+ * see `codeInput.Plugin`.
215
+ */
216
+ plugins = [];
217
+ },
218
+
219
+
220
+ /* ------------------------------------
221
+ * ------------Plugins-----------------
222
+ * ------------------------------------ */
223
+
224
+ /**
225
+ * Before using any plugin in this namespace, please ensure you import its JavaScript
226
+ * files (in the plugins folder), or continue to get a more detailed error in the developer
227
+ * console.
228
+ *
229
+ * Where plugins are stored, after they are imported. The plugin
230
+ * file assigns them a space in this object.
231
+ * For adding completely new syntax-highlighting algorithms, please see `codeInput.templates`.
232
+ *
233
+ * Key - plugin name
234
+ *
235
+ * Value - plugin object
236
+ * @type {Object}
237
+ */
238
+ plugins: new Proxy({}, {
239
+ get(plugins, name) {
240
+ if(plugins[name] == undefined) {
241
+ throw ReferenceError(`code-input: Plugin '${name}' is not defined. Please ensure you import the necessary files from the plugins folder in the WebCoder49/code-input repository, in the <head> of your HTML, before the plugin is instatiated.`);
242
+ }
243
+ return plugins[name];
244
+ }
245
+ }),
246
+
247
+ /**
248
+ * Plugins are imported from the plugins folder. They will then
249
+ * provide custom extra functionality to code-input elements.
250
+ */
251
+ Plugin: class {
252
+ /**
253
+ * Create a Plugin
254
+ *
255
+ * @param {Array<string>} observedAttributes - The HTML attributes to watch for this plugin, and report any
256
+ * modifications to the `codeInput.Plugin.attributeChanged` method.
257
+ */
258
+ constructor(observedAttributes) {
259
+
260
+ observedAttributes.forEach((attribute) => {
261
+ codeInput.observedAttributes.push(attribute);
262
+ });
263
+ }
264
+
265
+ /**
266
+ * Replace the values in destination with those from source where the keys match, in-place.
267
+ * @param {Object} destination Where to place the translated strings, already filled with the keys pointing to English strings.
268
+ * @param {Object} source The same keys, or some of them, mapped to translated strings. Keys not present here will retain the values they are maapped to in destination.
269
+ */
270
+ addTranslations(destination, source) {
271
+ for(const key in source) {
272
+ destination[key] = source[key];
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Runs before code is highlighted.
278
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
279
+ */
280
+ beforeHighlight(codeInput) { }
281
+ /**
282
+ * Runs after code is highlighted.
283
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
284
+ */
285
+ afterHighlight(codeInput) { }
286
+ /**
287
+ * Runs before elements are added into a code-input element.
288
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
289
+ */
290
+ beforeElementsAdded(codeInput) { }
291
+ /**
292
+ * Runs after elements are added into a code-input element (useful for adding events to the textarea).
293
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
294
+ */
295
+ afterElementsAdded(codeInput) { }
296
+ /**
297
+ * Runs when an attribute of a code-input element is changed (you must add the attribute name to `codeInput.Plugin.observedAttributes` first).
298
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
299
+ * @param {string} name - The name of the attribute
300
+ * @param {string} oldValue - The value of the attribute before it was changed
301
+ * @param {string} newValue - The value of the attribute after it is changed
302
+ */
303
+ attributeChanged(codeInput, name, oldValue, newValue) { }
304
+ },
305
+
306
+ /* ------------------------------------
307
+ * -------------Main-------------------
308
+ * ------------------------------------ */
309
+
310
+ /**
311
+ * A `<code-input>` element, an instance of an `HTMLElement`, and the result
312
+ * of `document.createElement("code-input")`. Attributes are only set when
313
+ * the element's template has been registered, and before this are null.
314
+ */
315
+ CodeInput: class extends HTMLElement {
316
+ constructor() {
317
+ super(); // Element
318
+ }
319
+
320
+ /**
321
+ * When the code-input's template is registered, this contains its codeInput.Template object.
322
+ * Should be treated as read-only by external code.
323
+ */
324
+ templateObject = null;
325
+ /**
326
+ * Exposed child textarea element for user to input code in
327
+ */
328
+ textareaElement = null;
329
+ /**
330
+ * Exposed child pre element where syntax-highlighted code is outputted.
331
+ * Contains this.codeElement as its only child.
332
+ */
333
+ preElement = null
334
+ /**
335
+ * Exposed child pre element's child code element where syntax-highlighted code is outputted.
336
+ * Has this.preElement as its parent.
337
+ */
338
+ codeElement = null;
339
+
340
+ /**
341
+ * Exposed non-scrolling element designed to contain dialog boxes etc. from plugins,
342
+ * that shouldn't scroll with the code-input element.
343
+ */
344
+ dialogContainerElement = null;
345
+
346
+ /**
347
+ * Form-Associated Custom Element Callbacks
348
+ * https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example
349
+ */
350
+ static formAssociated = true;
351
+
352
+ /**
353
+ * When events are transferred to the textarea element, callbacks
354
+ * are bound to set the this variable to the code-input element
355
+ * rather than the textarea. This allows the callback to be converted
356
+ * to a bound one:
357
+ * Key - Callback not bound
358
+ * Value - Callback that is bound, with this equalling the code-input element in the callback
359
+ */
360
+ boundEventCallbacks = {};
361
+
362
+ /** Trigger this event in all plugins with a optional list of arguments
363
+ * @param {string} eventName - the name of the event to trigger
364
+ * @param {Array} args - the arguments to pass into the event callback in the template after the code-input element. Normally left empty
365
+ */
366
+ pluginEvt(eventName, args) {
367
+ for (let i in this.templateObject.plugins) {
368
+ let plugin = this.templateObject.plugins[i];
369
+ if (eventName in plugin) {
370
+ if (args === undefined) {
371
+ plugin[eventName](this);
372
+ } else {
373
+ plugin[eventName](this, ...args);
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ /* ------------------------------------
380
+ * ----------Main Functionality--------
381
+ * ------------------------------------
382
+ * The main function of a code-input element is to take
383
+ * code written in its textarea element, copy this code into
384
+ * the result (pre code) element, then use the template object
385
+ * to syntax-highlight it. */
386
+
387
+ needsHighlight = false; // Just inputted
388
+ originalAriaDescription;
389
+
390
+ /**
391
+ * Highlight the code as soon as possible
392
+ */
393
+ scheduleHighlight() {
394
+ this.needsHighlight = true;
395
+ }
396
+
397
+ /**
398
+ * Call an animation frame
399
+ */
400
+ animateFrame() {
401
+ // Synchronise the contents of the pre/code and textarea elements
402
+ if(this.needsHighlight) {
403
+ this.update();
404
+ this.needsHighlight = false;
405
+ }
406
+
407
+ window.requestAnimationFrame(this.animateFrame.bind(this));
408
+ }
409
+
410
+ /**
411
+ * Update the text value to the result element, after the textarea contents have changed.
412
+ */
413
+ update() {
414
+ let resultElement = this.codeElement;
415
+ let value = this.value;
416
+ value += "\n"; // Placeholder for next line
417
+
418
+ // Update code
419
+ resultElement.innerHTML = this.escapeHtml(value);
420
+ this.pluginEvt("beforeHighlight");
421
+
422
+ // Syntax Highlight
423
+ if (this.templateObject.includeCodeInputInHighlightFunc) this.templateObject.highlight(resultElement, this);
424
+ else this.templateObject.highlight(resultElement);
425
+
426
+ this.syncSize();
427
+
428
+ this.pluginEvt("afterHighlight");
429
+ }
430
+
431
+ /**
432
+ * Set the size of the textarea element to the size of the pre/code element.
433
+ */
434
+ syncSize() {
435
+ // Synchronise the size of the pre/code and textarea elements
436
+ if(this.templateObject.preElementStyled) {
437
+ this.style.backgroundColor = getComputedStyle(this.preElement).backgroundColor;
438
+ this.textareaElement.style.height = getComputedStyle(this.preElement).height;
439
+ this.textareaElement.style.width = getComputedStyle(this.preElement).width;
440
+ } else {
441
+ this.style.backgroundColor = getComputedStyle(this.codeElement).backgroundColor;
442
+ this.textareaElement.style.height = getComputedStyle(this.codeElement).height;
443
+ this.textareaElement.style.width = getComputedStyle(this.codeElement).width;
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Show some instructions to the user only if they are using keyboard navigation - for example, a prompt on how to navigate with the keyboard if Tab is repurposed.
449
+ * @param {string} instructions The instructions to display only if keyboard navigation is being used. If it's blank, no instructions will be shown.
450
+ * @param {boolean} includeAriaDescriptionFirst Whether to include the aria-description of the code-input element before the keyboard navigation instructions for a screenreader. Keep this as true when the textarea is first focused.
451
+ */
452
+ setKeyboardNavInstructions(instructions, includeAriaDescriptionFirst) {
453
+ this.dialogContainerElement.querySelector(".code-input_keyboard-navigation-instructions").innerText = instructions;
454
+ if(includeAriaDescriptionFirst) {
455
+ this.textareaElement.setAttribute("aria-description", this.originalAriaDescription + ". " + instructions);
456
+ } else {
457
+ this.textareaElement.setAttribute("aria-description", instructions);
458
+ }
459
+ }
460
+
461
+ /**
462
+ * HTML-escape an arbitrary string.
463
+ * @param {string} text - The original, unescaped text
464
+ * @returns {string} - The new, HTML-escaped text
465
+ */
466
+ escapeHtml(text) {
467
+ return text.replace(new RegExp("&", "g"), "&amp;").replace(new RegExp("<", "g"), "&lt;"); /* Global RegExp */
468
+ }
469
+
470
+ /**
471
+ * HTML-unescape an arbitrary string.
472
+ * @param {string} text - The original, HTML-escaped text
473
+ * @returns {string} - The new, unescaped text
474
+ */
475
+ unescapeHtml(text) {
476
+ return text.replace(new RegExp("&amp;", "g"), "&").replace(new RegExp("&lt;", "g"), "<").replace(new RegExp("&gt;", "g"), ">"); /* Global RegExp */
477
+ }
478
+
479
+ /**
480
+ * Get the template object this code-input element is using.
481
+ * @returns {Object} - Template object
482
+ */
483
+ getTemplate() {
484
+ let templateName;
485
+ if (this.getAttribute("template") == undefined) {
486
+ // Default
487
+ templateName = codeInput.defaultTemplate;
488
+ } else {
489
+ templateName = this.getAttribute("template");
490
+ }
491
+ if (templateName in codeInput.usedTemplates) {
492
+ return codeInput.usedTemplates[templateName];
493
+ } else {
494
+ // Doesn't exist - add to queue
495
+ if (!(templateName in codeInput.templateNotYetRegisteredQueue)) {
496
+ codeInput.templateNotYetRegisteredQueue[templateName] = [];
497
+ }
498
+ codeInput.templateNotYetRegisteredQueue[templateName].push(this);
499
+ return undefined;
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Set up and initialise the textarea.
505
+ * This will be called once the template has been added.
506
+ */
507
+ setup() {
508
+ if(this.textareaElement != null) return; // Already set up
509
+
510
+ this.classList.add("code-input_registered");
511
+
512
+ this.mutationObserver = new MutationObserver(this.mutationObserverCallback.bind(this));
513
+ this.mutationObserver.observe(this, {
514
+ attributes: true,
515
+ attributeOldValue: true
516
+ });
517
+
518
+ this.classList.add("code-input_registered"); // Remove register message
519
+ if (this.templateObject.preElementStyled) this.classList.add("code-input_pre-element-styled");
520
+
521
+ this.pluginEvt("beforeElementsAdded");
522
+
523
+ const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]");
524
+ let value;
525
+ if(fallbackTextarea) {
526
+ // Fallback textarea exists
527
+ // Sync attributes; existing code-input attributes take priority
528
+ let textareaAttributeNames = fallbackTextarea.getAttributeNames();
529
+ for(let i = 0; i < textareaAttributeNames.length; i++) {
530
+ const attr = textareaAttributeNames[i];
531
+ if(attr == "data-code-input-fallback") continue;
532
+
533
+ if(!this.hasAttribute(attr)) {
534
+ this.setAttribute(attr, fallbackTextarea.getAttribute(attr));
535
+ }
536
+ }
537
+ // Sync value
538
+ value = fallbackTextarea.value;
539
+ } else {
540
+ value = this.unescapeHtml(this.innerHTML);
541
+ }
542
+ value = value || this.getAttribute("value") || "";
543
+
544
+ // First-time attribute sync
545
+ const lang = this.getAttribute("language") || this.getAttribute("lang");
546
+ const placeholder = this.getAttribute("placeholder") || lang || "";
547
+
548
+
549
+ this.initialValue = value; // For form reset
550
+
551
+ // Create textarea
552
+ const textarea = document.createElement("textarea");
553
+ textarea.placeholder = placeholder;
554
+ if(value != "") {
555
+ textarea.value = value;
556
+ }
557
+ textarea.innerHTML = this.innerHTML;
558
+ if(!this.hasAttribute("spellcheck")) {
559
+ // For backwards compatibility:
560
+ textarea.setAttribute("spellcheck", "false");
561
+ }
562
+
563
+ // Disable focusing on the code-input element - only allow the textarea to be focusable
564
+ textarea.setAttribute("tabindex", this.getAttribute("tabindex") || 0);
565
+ this.setAttribute("tabindex", -1);
566
+ // Save aria-description so keyboard navigation guidance can be added.
567
+ this.originalAriaDescription = this.getAttribute("aria-description") || "Code input field";
568
+
569
+ // Accessibility - detect when mouse focus to remove focus outline + keyboard navigation guidance that could irritate users.
570
+ this.addEventListener("mousedown", () => {
571
+ this.classList.add("code-input_mouse-focused");
572
+ });
573
+ textarea.addEventListener("blur", () => {
574
+ this.classList.remove("code-input_mouse-focused");
575
+ });
576
+
577
+ this.innerHTML = ""; // Clear Content
578
+
579
+ // Synchronise attributes to textarea
580
+ for(let i = 0; i < this.attributes.length; i++) {
581
+ let attribute = this.attributes[i].name;
582
+ if (codeInput.textareaSyncAttributes.includes(attribute) || attribute.substring(0, 5) == "aria-") {
583
+ textarea.setAttribute(attribute, this.getAttribute(attribute));
584
+ }
585
+ }
586
+
587
+ textarea.addEventListener('input', (evt) => { this.value = this.textareaElement.value; });
588
+
589
+ // Save element internally
590
+ this.textareaElement = textarea;
591
+ this.append(textarea);
592
+
593
+ // Create result element
594
+ let code = document.createElement("code");
595
+ let pre = document.createElement("pre");
596
+ pre.setAttribute("aria-hidden", "true"); // Hide for screen readers
597
+ pre.setAttribute("tabindex", "-1"); // Hide for keyboard navigation
598
+ pre.setAttribute("inert", true); // Hide for keyboard navigation
599
+
600
+ // Save elements internally
601
+ this.preElement = pre;
602
+ this.codeElement = code;
603
+ pre.append(code);
604
+ this.append(pre);
605
+
606
+ if (this.templateObject.isCode) {
607
+ if (lang != undefined && lang != "") {
608
+ code.classList.add("language-" + lang.toLowerCase());
609
+ }
610
+ }
611
+
612
+ // dialogContainerElement used to store non-scrolling dialog boxes, etc.
613
+ let dialogContainerElement = document.createElement("div");
614
+ dialogContainerElement.classList.add("code-input_dialog-container");
615
+ this.append(dialogContainerElement);
616
+ this.dialogContainerElement = dialogContainerElement;
617
+
618
+ let keyboardNavigationInstructions = document.createElement("div");
619
+ keyboardNavigationInstructions.classList.add("code-input_keyboard-navigation-instructions");
620
+ dialogContainerElement.append(keyboardNavigationInstructions);
621
+
622
+ this.pluginEvt("afterElementsAdded");
623
+
624
+ this.dispatchEvent(new CustomEvent("code-input_load"));
625
+
626
+ this.value = value;
627
+ this.animateFrame();
628
+
629
+ const resizeObserver = new ResizeObserver((elements) => {
630
+ // The only element that could be resized is this code-input element.
631
+ this.syncSize();
632
+ });
633
+ resizeObserver.observe(this);
634
+
635
+ this.classList.add("code-input_loaded");
636
+ }
637
+
638
+ /**
639
+ * @deprecated This shouldn't have been accessed as part of the library's public interface (to enable more flexibility in backwards-compatible versions), but is still here just in case it was.
640
+ */
641
+ escape_html(text) {
642
+ return this.escapeHtml(text);
643
+ }
644
+
645
+ /**
646
+ * @deprecated This shouldn't have been accessed as part of the library's public interface (to enable more flexibility in backwards-compatible versions), but is still here just in case it was.
647
+ */
648
+ get_template() {
649
+ return this.getTemplate();
650
+ }
651
+
652
+ /**
653
+ * @deprecated Present for backwards compatibility; use CodeInput.templateObject.
654
+ */
655
+ get template() {
656
+ return this.templateObject;
657
+ }
658
+
659
+ /**
660
+ * @deprecated The Vue framework may try to set the template
661
+ * property to the value of the template attribute, a string.
662
+ * This should not happen. Intentional use of this should
663
+ * also not happen since templates are changed by changing
664
+ * the template attribute to the name of one registered.
665
+ */
666
+ set template(value) { }
667
+
668
+ /* ------------------------------------
669
+ * -----------Callbacks----------------
670
+ * ------------------------------------
671
+ * Implement the `HTMLElement` callbacks
672
+ * to trigger the main functionality properly. */
673
+
674
+ /**
675
+ * When the code-input element has been added to the document,
676
+ * find its template and set up the element.
677
+ */
678
+ connectedCallback() {
679
+ // Stored in templateObject because some frameworks will override
680
+ // template property with the string value of the attribute
681
+ this.templateObject = this.getTemplate();
682
+ if (this.templateObject != undefined) {
683
+ // Template registered before loading
684
+ this.classList.add("code-input_registered");
685
+ // Children not yet present - wait until they are
686
+ window.addEventListener("DOMContentLoaded", this.setup.bind(this))
687
+ }
688
+ }
689
+
690
+ mutationObserverCallback(mutationList, observer) {
691
+ for (const mutation of mutationList) {
692
+ if (mutation.type !== 'attributes')
693
+ continue;
694
+
695
+ /* Check regular attributes */
696
+ for(let i = 0; i < codeInput.observedAttributes.length; i++) {
697
+ if (mutation.attributeName == codeInput.observedAttributes[i]) {
698
+ return this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, super.getAttribute(mutation.attributeName));
699
+ }
700
+ }
701
+ for(let i = 0; i < codeInput.textareaSyncAttributes.length; i++) {
702
+ if (mutation.attributeName == codeInput.textareaSyncAttributes[i]) {
703
+ return this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, super.getAttribute(mutation.attributeName));
704
+ }
705
+ }
706
+ if (mutation.attributeName.substring(0, 5) == "aria-") {
707
+ return this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, super.getAttribute(mutation.attributeName));
708
+ }
709
+ }
710
+ }
711
+
712
+ disconnectedCallback() {
713
+ this.mutationObserver.disconnect();
714
+ }
715
+
716
+ /**
717
+ * Triggered when an observed HTML attribute
718
+ * has been modified (called from `mutationObserverCallback`).
719
+ * @param {string} name - The name of the attribute
720
+ * @param {string} oldValue - The value of the attribute before it was changed
721
+ * @param {string} newValue - The value of the attribute after it is changed
722
+ */
723
+ attributeChangedCallback(name, oldValue, newValue) {
724
+ if (this.isConnected) {
725
+ this.pluginEvt("attributeChanged", [name, oldValue, newValue]);
726
+ switch (name) {
727
+
728
+ case "value":
729
+ this.value = newValue;
730
+ break;
731
+ case "template":
732
+ this.templateObject = codeInput.usedTemplates[newValue || codeInput.defaultTemplate];
733
+ if (this.templateObject.preElementStyled) this.classList.add("code-input_pre-element-styled");
734
+ else this.classList.remove("code-input_pre-element-styled");
735
+ // Syntax Highlight
736
+ this.scheduleHighlight();
737
+
738
+ break;
739
+
740
+ case "lang":
741
+ case "language":
742
+ let code = this.codeElement;
743
+ let mainTextarea = this.textareaElement;
744
+
745
+ // Check not already updated
746
+ if (newValue != null) {
747
+ newValue = newValue.toLowerCase();
748
+
749
+ if (code.classList.contains(`language-${newValue}`)) break; // Already updated
750
+ }
751
+
752
+ if(oldValue !== null) {
753
+ // Case insensitive
754
+ oldValue = oldValue.toLowerCase();
755
+
756
+ // Remove old language class and add new
757
+ code.classList.remove("language-" + oldValue); // From codeElement
758
+ code.parentElement.classList.remove("language-" + oldValue); // From preElement
759
+ }
760
+ // Add new language class
761
+ code.classList.remove("language-none"); // Prism
762
+ code.parentElement.classList.remove("language-none"); // Prism
763
+
764
+ if (newValue != undefined && newValue != "") {
765
+ code.classList.add("language-" + newValue);
766
+ }
767
+
768
+ if (mainTextarea.placeholder == oldValue) mainTextarea.placeholder = newValue;
769
+
770
+ this.scheduleHighlight();
771
+
772
+ break;
773
+ default:
774
+ if (codeInput.textareaSyncAttributes.includes(name) || name.substring(0, 5) == "aria-") {
775
+ if(newValue == null || newValue == undefined) {
776
+ this.textareaElement.removeAttribute(name);
777
+ } else {
778
+ this.textareaElement.setAttribute(name, newValue);
779
+ }
780
+ } else {
781
+ codeInput.textareaSyncAttributes.regexp.forEach((attribute) => {
782
+ if (name.match(attribute)) {
783
+ if(newValue == null) {
784
+ this.textareaElement.removeAttribute(name);
785
+ } else {
786
+ this.textareaElement.setAttribute(name, newValue);
787
+ }
788
+ }
789
+ });
790
+ }
791
+ break;
792
+ }
793
+ }
794
+
795
+ }
796
+
797
+ /* ------------------------------------
798
+ * -----------Overrides----------------
799
+ * ------------------------------------
800
+ * Override/Implement ordinary HTML textarea functionality so that the <code-input>
801
+ * element acts just like a <textarea>. */
802
+
803
+ /**
804
+ * @override
805
+ */
806
+ addEventListener(type, listener, options = undefined) {
807
+ // Save a copy of the callback where `this` refers to the code-input element.
808
+ let boundCallback = function (evt) {
809
+ if (typeof listener === 'function') {
810
+ listener(evt);
811
+ } else if (listener && listener.handleEvent) {
812
+ listener.handleEvent(evt);
813
+ }
814
+ }.bind(this);
815
+ this.boundEventCallbacks[listener] = boundCallback;
816
+
817
+ if (codeInput.textareaSyncEvents.includes(type)) {
818
+ // Synchronise with textarea
819
+ this.boundEventCallbacks[listener] = boundCallback;
820
+
821
+ if (options === undefined) {
822
+ if(this.textareaElement == null) {
823
+ this.addEventListener("code-input_load", () => { this.textareaElement.addEventListener(type, boundCallback); });
824
+ } else {
825
+ this.textareaElement.addEventListener(type, boundCallback);
826
+ }
827
+ } else {
828
+ if(this.textareaElement == null) {
829
+ this.addEventListener("code-input_load", () => { this.textareaElement.addEventListener(type, boundCallback, options); });
830
+ } else {
831
+ this.textareaElement.addEventListener(type, boundCallback, options);
832
+ }
833
+ }
834
+ } else {
835
+ // Synchronise with code-input element
836
+ if (options === undefined) {
837
+ super.addEventListener(type, boundCallback);
838
+ } else {
839
+ super.addEventListener(type, boundCallback, options);
840
+ }
841
+ }
842
+ }
843
+
844
+ /**
845
+ * @override
846
+ */
847
+ removeEventListener(type, listener, options = undefined) {
848
+ // Save a copy of the callback where `this` refers to the code-input element
849
+ let boundCallback = this.boundEventCallbacks[listener];
850
+
851
+ if (codeInput.textareaSyncEvents.includes(type)) {
852
+ // Synchronise with textarea
853
+ if (options === undefined) {
854
+ if(this.textareaElement == null) {
855
+ this.addEventListener("code-input_load", () => { this.textareaElement.removeEventListener(type, boundCallback); });
856
+ } else {
857
+ this.textareaElement.removeEventListener(type, boundCallback);
858
+ }
859
+ } else {
860
+ if(this.textareaElement == null) {
861
+ this.addEventListener("code-input_load", () => { this.textareaElement.removeEventListener(type, boundCallback, options); });
862
+ } else {
863
+ this.textareaElement.removeEventListener(type, boundCallback, options);
864
+ }
865
+ }
866
+ } else {
867
+ // Synchronise with code-input element
868
+ if (options === undefined) {
869
+ super.removeEventListener(type, boundCallback);
870
+ } else {
871
+ super.removeEventListener(type, boundCallback, options);
872
+ }
873
+ }
874
+ }
875
+
876
+ //-------------------------------------------
877
+ //----------- Textarea interface ------------
878
+ //-------------------------------------------
879
+ // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLTextAreaElement
880
+ // Attributes defined at codeInput.textareaSyncAttributes
881
+
882
+ /**
883
+ * Get the JavaScript property from the internal textarea
884
+ * element, given its name and a defaultValue to return
885
+ * when no textarea is present (undefined to make it throw
886
+ * an error instead).
887
+ *
888
+ * For internal use - treat the code-input element as a
889
+ * textarea for the standard properties (e.g. document.
890
+ * querySelector("code-input").defaultValue).
891
+ */
892
+ getTextareaProperty(name, defaultValue=undefined) {
893
+ if(this.textareaElement) {
894
+ return this.textareaElement[name];
895
+ } else {
896
+ // Unregistered
897
+ const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]");
898
+ if(fallbackTextarea) {
899
+ return fallbackTextarea[name];
900
+ } else {
901
+ if(defaultValue === undefined) {
902
+ throw new Error("Cannot get "+name+" of an unregistered code-input element without a data-code-input-fallback textarea.");
903
+ }
904
+ return defaultValue;
905
+ }
906
+ }
907
+ }
908
+ /**
909
+ * Set the JavaScript property of the internal textarea
910
+ * element, given its name and value.
911
+ *
912
+ * If there is no registered or fallback textarea and errorIfCannot is
913
+ * false, return false (otherwise true); If there is no registered or
914
+ * fallback textarea and errorIfCannot is true, throw an error.
915
+ *
916
+ * For internal use - treat the code-input element as a
917
+ * textarea for the standard properties (e.g. document.
918
+ * querySelector("code-input").defaultValue).
919
+ */
920
+ setTextareaProperty(name, value, errorIfCannot=true) {
921
+ if(this.textareaElement) {
922
+ this.textareaElement[name] = value;
923
+ } else {
924
+ // Unregistered
925
+ const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]");
926
+ if(fallbackTextarea) {
927
+ fallbackTextarea[name] = value;
928
+ } else {
929
+ if(!errorIfCannot) return false;
930
+ throw new Error("Cannot set "+name+" of an unregistered code-input element without a data-code-input-fallback textarea.");
931
+ }
932
+ }
933
+ return true;
934
+ }
935
+
936
+ get autocomplete() { return this.getAttribute("autocomplete"); }
937
+ set autocomplete(val) { return this.setAttribute("autocomplete", val); }
938
+ get cols() { return this.getTextareaProperty("cols", Number(this.getAttribute("cols"))); }
939
+ set cols(val) { this.setAttribute("cols", val); }
940
+ get defaultValue() { return this.initialValue; }
941
+ set defaultValue(val) { this.initialValue = val; }
942
+ get textContent() { return this.initialValue; }
943
+ set textContent(val) { this.initialValue = val; }
944
+ get dirName() { return this.getAttribute("dirName") || ""; }
945
+ set dirName(val) { this.setAttribute("dirname", val); }
946
+ get disabled() { return this.hasAttribute("disabled"); }
947
+ set disabled(val) {
948
+ if(val) {
949
+ this.setAttribute("disabled", true);
950
+ } else {
951
+ this.removeAttribute("disabled");
952
+ }
953
+ }
954
+ get form() { return this.getTextareaProperty("form"); }
955
+ get labels() { return this.getTextareaProperty("labels"); }
956
+ get maxLength() {
957
+ const result = Number(this.getAttribute("maxlength"));
958
+ if(isNaN(result)) {
959
+ return -1;
960
+ }
961
+ return result;
962
+ }
963
+ set maxLength(val) {
964
+ if(val == -1) {
965
+ this.removeAttribute("maxlength");
966
+ } else {
967
+ this.setAttribute("maxlength", val);
968
+ }
969
+ }
970
+ get minLength() {
971
+ const result = Number(this.getAttribute("minlength"));
972
+ if(isNaN(result)) {
973
+ return -1;
974
+ }
975
+ return result;
976
+ }
977
+ set minLength(val) {
978
+ if(val == -1) {
979
+ this.removeAttribute("minlength");
980
+ } else {
981
+ this.setAttribute("minlength", val);
982
+ }
983
+ }
984
+ get name() { return this.getAttribute("name") || ""; }
985
+ set name(val) { this.setAttribute("name", val); }
986
+ get placeholder() { return this.getAttribute("placeholder") || ""; }
987
+ set placeholder(val) { this.setAttribute("placeholder", val); }
988
+ get readOnly() { return this.hasAttribute("readonly"); }
989
+ set readOnly(val) {
990
+ if(val) {
991
+ this.setAttribute("readonly", true);
992
+ } else {
993
+ this.removeAttribute("readonly");
994
+ }
995
+ }
996
+ get required() { return this.hasAttribute("readonly"); }
997
+ set required(val) {
998
+ if(val) {
999
+ this.setAttribute("readonly", true);
1000
+ } else {
1001
+ this.removeAttribute("readonly");
1002
+ }
1003
+ }
1004
+ get rows() { return this.getTextareaProperty("rows", Number(this.getAttribute("rows"))); }
1005
+ set rows(val) { this.setAttribute("rows", val); }
1006
+ get selectionDirection() { return this.getTextareaProperty("selectionDirection"); }
1007
+ set selectionDirection(val) { this.setTextareaProperty("selectionDirection", val); }
1008
+ get selectionEnd() { return this.getTextareaProperty("selectionEnd"); }
1009
+ set selectionEnd(val) { this.setTextareaProperty("selectionEnd", val); }
1010
+ get selectionStart() { return this.getTextareaProperty("selectionStart"); }
1011
+ set selectionStart(val) { this.setTextareaProperty("selectionStart", val); }
1012
+ get textLength() { return this.value.length; }
1013
+ get type() { return "textarea"; } // Mimics textarea
1014
+ get validationMessage() { return this.getTextareaProperty("validationMessage"); }
1015
+ get validity() { return this.getTextareaProperty("validationMessage"); }
1016
+ get value() { return this.getTextareaProperty("value", this.getAttribute("value") || this.innerHTML); }
1017
+ set value(val) {
1018
+ val = val || "";
1019
+ if(this.setTextareaProperty("value", val, false)) {
1020
+ if(this.textareaElement) this.scheduleHighlight();
1021
+ } else {
1022
+ this.innerHTML = val;
1023
+ }
1024
+ }
1025
+ get willValidate() { return this.getTextareaProperty("willValidate", this.disabled || this.readOnly); }
1026
+ get wrap() { return this.getAttribute("wrap") || ""; }
1027
+ set wrap(val) { this.setAttribute("wrap", val); }
1028
+
1029
+
1030
+ blur(options={}) { return this.textareaElement.blur(options); }
1031
+ checkValidity() { return this.textareaElement.checkValidity(); }
1032
+ focus(options={}) { return this.textareaElement.focus(options); }
1033
+ reportValidity() { return this.textareaElement.reportValidity(); }
1034
+ setCustomValidity(error) { this.textareaElement.setCustomValidity(error); }
1035
+ setRangeText(replacement, selectionStart=this.selectionStart, selectionEnd=this.selectionEnd, selectMode="preserve") { this.getTextareaProperty("setRangeText")(replacement, selectionStart, selectionEnd, selectMode); }
1036
+ setSelectionRange(selectionStart, selectionEnd, selectionDirection="none") { this.getTextareaProperty("setSelectionRange")(selectionStart, selectionEnd, selectionDirection); }
1037
+
1038
+ /**
1039
+ * Allows plugins to store data in the scope of a single element.
1040
+ * Key - name of the plugin, in camelCase
1041
+ * Value - object of data to be stored; different plugins may use this differently.
1042
+ */
1043
+ pluginData = {};
1044
+
1045
+ /**
1046
+ * Update value on form reset
1047
+ */
1048
+ formResetCallback() {
1049
+ this.value = this.initialValue;
1050
+ };
1051
+ }
1052
+ }
1053
+
1054
+
1055
+ customElements.define("code-input", codeInput.CodeInput);
1056
+ export const Plugin = codeInput.Plugin;
1057
+ export const Template = codeInput.Template;
1058
+ export const CodeInput = codeInput.CodeInput;
1059
+ export const registerTemplate = codeInput.registerTemplate;
1060
+ export default { Plugin, Template, CodeInput, registerTemplate };