@webcoder49/code-input 1.5.1 → 2.0.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
@@ -5,6 +5,8 @@
5
5
  *
6
6
  * **<https://github.com/WebCoder49/code-input>**
7
7
  */
8
+
9
+
8
10
  var codeInput = {
9
11
  /**
10
12
  * A list of attributes that will trigger the
@@ -27,6 +29,7 @@ var codeInput = {
27
29
  * code-input element.
28
30
  */
29
31
  textareaSyncAttributes: [
32
+ "aria-*",
30
33
  "value",
31
34
  "name",
32
35
  // Form validation - https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation#using_built-in_form_validation
@@ -85,17 +88,19 @@ var codeInput = {
85
88
  * @param {Object} template - a Template object instance - see `codeInput.templates`
86
89
  */
87
90
  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.`);
91
+ if(!(typeof templateName == "string" || templateName instanceof String)) throw TypeError(`code-input: Template for "${templateName}" must be a string.`);
92
+ 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.`);
93
+ 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.`);
94
+ 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.`);
95
+ 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.`);
96
+ 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.`);
97
+
94
98
  template.plugins.forEach((plugin, i) => {
95
99
  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.`);
100
+ 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.`);
97
101
  }
98
102
  });
103
+
99
104
 
100
105
  codeInput.usedTemplates[templateName] = template;
101
106
  // Add waiting code-input elements wanting this template from queue
@@ -103,10 +108,13 @@ var codeInput = {
103
108
  for (let i in codeInput.templateNotYetRegisteredQueue[templateName]) {
104
109
  elem = codeInput.templateNotYetRegisteredQueue[templateName][i];
105
110
  elem.template = template;
106
- elem.setup();
111
+ codeInput.runOnceWindowLoaded((function(elem) { elem.connectedCallback(); }).bind(null, elem), elem);
112
+ // Bind sets elem in parameter
113
+ // So innerHTML can be read
107
114
  }
108
115
  console.log(`code-input: template: Added existing elements with template ${templateName}`);
109
116
  }
117
+
110
118
  if (codeInput.defaultTemplate == undefined) {
111
119
  codeInput.defaultTemplate = templateName;
112
120
  // Add elements with default template from queue
@@ -114,7 +122,9 @@ var codeInput = {
114
122
  for (let i in codeInput.templateNotYetRegisteredQueue[undefined]) {
115
123
  elem = codeInput.templateNotYetRegisteredQueue[undefined][i];
116
124
  elem.template = template;
117
- elem.setup();
125
+ codeInput.runOnceWindowLoaded((function(elem) { elem.connectedCallback(); }).bind(null, elem), elem);
126
+ // Bind sets elem in parameter
127
+ // So innerHTML can be read
118
128
  }
119
129
  }
120
130
  console.log(`code-input: template: Set template ${templateName} as default`);
@@ -123,34 +133,80 @@ var codeInput = {
123
133
  },
124
134
 
125
135
  /**
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`.
136
+ * Please see `codeInput.templates.prism` or `codeInput.templates.hljs`.
137
+ * Templates are used in `<code-input>` elements and once registered with
138
+ * `codeInput.registerTemplate` will be in charge of the highlighting
139
+ * algorithm and settings for all code-inputs with a `template` attribute
140
+ * matching the registered name.
133
141
  */
134
- templates: {
142
+ Template: class {
135
143
  /**
136
144
  * Constructor to create a custom template instance. Pass this into `codeInput.registerTemplate` to use it.
137
145
  * I would strongly recommend using the built-in simpler template `codeInput.templates.prism` or `codeInput.templates.hljs`.
138
146
  * @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.
147
+ * @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
148
  * @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
149
  * @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`
150
+ * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.Plugin`
143
151
  * @returns template object
144
152
  */
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
- },
153
+ constructor(highlight = function () { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, plugins = []) {
154
+ this.highlight = highlight;
155
+ this.preElementStyled = preElementStyled;
156
+ this.isCode = isCode;
157
+ this.includeCodeInputInHighlightFunc = includeCodeInputInHighlightFunc;
158
+ this.plugins = plugins;
159
+ }
160
+
161
+ /**
162
+ * A callback to highlight the code, that takes an HTML `<code>` element
163
+ * inside a `<pre>` element as a parameter, and an optional second
164
+ * `<code-input>` element parameter if `this.includeCodeInputInHighlightFunc` is
165
+ * `true`.
166
+ */
167
+ highlight = function() {};
168
+
169
+ /**
170
+ * Is the <pre> element CSS-styled as well as the `<code>` element?
171
+ * If `true`, `<pre>` element's scrolling is synchronised; if false,
172
+ * <code> element's scrolling is synchronised.
173
+ */
174
+ preElementStyled = true;
175
+
176
+ /**
177
+ * Is this for writing code?
178
+ * If true, the code-input's lang HTML attribute can be used,
179
+ * and the `<code>` element will be given the class name
180
+ * 'language-[lang attribute's value]'.
181
+ */
182
+ isCode = true;
183
+
184
+ /**
185
+ * Setting this to true passes the `<code-input>` element as a
186
+ * second argument to the highlight function.
187
+ */
188
+ includeCodeInputInHighlightFunc = false;
189
+
190
+ /**
191
+ * An array of plugin objects to add extra features -
192
+ * see `codeInput.Plugin`.
193
+ */
194
+ plugins = [];
195
+ },
196
+
197
+ /**
198
+ * For creating a custom template from scratch, please
199
+ * use `new codeInput.Template(...)`
200
+ *
201
+ * Shortcut functions for creating templates.
202
+ * Each code-input element has a template attribute that
203
+ * tells it which template to use.
204
+ * Each template contains functions and preferences that
205
+ * run the syntax-highlighting and let code-input control
206
+ * the highlighting.
207
+ * For adding small pieces of functionality, please see `codeInput.plugins`.
208
+ */
209
+ templates: {
154
210
  /**
155
211
  * Constructor to create a template that uses Prism.js syntax highlighting (https://prismjs.com/)
156
212
  * @param {Object} prism Import Prism.js, then after that import pass the `Prism` object as this parameter.
@@ -245,7 +301,20 @@ var codeInput = {
245
301
  */
246
302
  rainbow_text(rainbowColors = ["red", "orangered", "orange", "goldenrod", "gold", "green", "darkgreen", "navy", "blue", "magenta"], delimiter = "", plugins = []) {
247
303
  return this.rainbowText(rainbowColors, delimiter, plugins);
248
- }
304
+ },
305
+
306
+ /**
307
+ * @deprecated Please use `new codeInput.Template()`
308
+ */
309
+ custom(highlight = function () { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, plugins = []) {
310
+ return {
311
+ highlight: highlight,
312
+ includeCodeInputInHighlightFunc: includeCodeInputInHighlightFunc,
313
+ preElementStyled: preElementStyled,
314
+ isCode: isCode,
315
+ plugins: plugins,
316
+ };
317
+ },
249
318
  },
250
319
 
251
320
  /* ------------------------------------
@@ -253,25 +322,52 @@ var codeInput = {
253
322
  * ------------------------------------ */
254
323
 
255
324
  /**
325
+ * Before using any plugin in this namespace, please ensure you import its JavaScript
326
+ * files (in the plugins folder), or continue to get a more detailed error in the developer
327
+ * console.
328
+ *
256
329
  * Where plugins are stored, after they are imported. The plugin
257
330
  * file assigns them a space in this object.
258
331
  * For adding completely new syntax-highlighting algorithms, please see `codeInput.templates`.
332
+ *
259
333
  * Key - plugin name
334
+ *
260
335
  * Value - plugin object
261
336
  * @type {Object}
262
337
  */
263
- plugins: {
264
- },
338
+ plugins: new Proxy({}, {
339
+ get(plugins, name) {
340
+ if(plugins[name] == undefined) {
341
+ 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.`);
342
+ }
343
+ return plugins[name];
344
+ }
345
+ }),
265
346
 
266
347
  /**
267
348
  * Plugins are imported from the plugins folder. They will then
268
349
  * provide custom extra functionality to code-input elements.
269
350
  */
270
351
  Plugin: class {
271
- constructor() {
352
+ /**
353
+ * Create a Plugin
354
+ *
355
+ * @param {Array<string>} observedAttributes - The HTML attributes to watch for this plugin, and report any
356
+ * modifications to the `codeInput.Plugin.attributeChanged` method.
357
+ */
358
+ constructor(observedAttributes) {
272
359
  console.log("code-input: plugin: Created plugin");
273
360
 
274
- codeInput.observedAttributes = codeInput.observedAttributes.concat(self.observedAttributes);
361
+ observedAttributes.forEach((attribute) => {
362
+ // Move plugin attribute to codeInput observed attributes
363
+ let regexFromWildcard = codeInput.wildcard2regex(attribute);
364
+ if(regexFromWildcard == null) {
365
+ // Not a wildcard
366
+ codeInput.observedAttributes.push(attribute);
367
+ } else {
368
+ codeInput.observedAttributes.regexp.push(regexFromWildcard);
369
+ }
370
+ });
275
371
  }
276
372
 
277
373
  /**
@@ -302,11 +398,6 @@ var codeInput = {
302
398
  * @param {string} newValue - The value of the attribute after it is changed
303
399
  */
304
400
  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
401
  },
311
402
 
312
403
  /* ------------------------------------
@@ -320,10 +411,35 @@ var codeInput = {
320
411
  constructor() {
321
412
  super(); // Element
322
413
  }
414
+ /**
415
+ * Store value internally
416
+ */
417
+ _value = '';
418
+
419
+ /**
420
+ * Exposed child textarea element for user to input code in
421
+ */
422
+ textareaElement = null;
423
+ /**
424
+ * Exposed child pre element where syntax-highlighted code is outputted.
425
+ * Contains this.codeElement as its only child.
426
+ */
427
+ preElement = null;
428
+ /**
429
+ * Exposed child pre element's child code element where syntax-highlighted code is outputted.
430
+ * Has this.preElement as its parent.
431
+ */
432
+ codeElement = null;
433
+
434
+ /**
435
+ * Form-Associated Custom Element Callbacks
436
+ * https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example
437
+ */
438
+ static formAssociated = true;
323
439
 
324
440
  /**
325
441
  * When events are transferred to the textarea element, callbacks
326
- * are bound to set the this variable to the code-inpute element
442
+ * are bound to set the this variable to the code-input element
327
443
  * rather than the textarea. This allows the callback to be converted
328
444
  * to a bound one:
329
445
  * Key - Callback not bound
@@ -358,6 +474,7 @@ var codeInput = {
358
474
 
359
475
  /** Update the text value to the result element, after the textarea contents have changed.
360
476
  * @param {string} value - The text value of the code-input element
477
+ * @param {boolean} originalUpdate - Whether this update originates from the textarea's content; if so, run it first so custom updates override it.
361
478
  */
362
479
  update(value) {
363
480
  // Prevent this from running multiple times on the same input when "value" attribute is changed,
@@ -365,13 +482,18 @@ var codeInput = {
365
482
  // been run). Thank you to peterprvy for this.
366
483
  if (this.ignoreValueUpdate) return;
367
484
 
485
+ if(this.textareaElement == null) {
486
+ this.addEventListener("code-input_load", () => { this.update(value) }); // Only run when fully loaded
487
+ return;
488
+ }
489
+
368
490
  this.ignoreValueUpdate = true;
369
491
  this.value = value;
370
492
  this.ignoreValueUpdate = false;
371
- if (this.querySelector("textarea").value != value) this.querySelector("textarea").value = value;
493
+ if (this.textareaElement.value != value) this.textareaElement.value = value;
372
494
 
373
495
 
374
- let resultElement = this.querySelector("pre code");
496
+ let resultElement = this.codeElement;
375
497
 
376
498
  // Handle final newlines
377
499
  if (value[value.length - 1] == "\n") {
@@ -393,8 +515,8 @@ var codeInput = {
393
515
  * Synchronise the scrolling of the textarea to the result element.
394
516
  */
395
517
  syncScroll() {
396
- let inputElement = this.querySelector("textarea");
397
- let resultElement = this.template.preElementStyled ? this.querySelector("pre") : this.querySelector("pre code");
518
+ let inputElement = this.textareaElement;
519
+ let resultElement = this.template.preElementStyled ? this.preElement : this.codeElement;
398
520
 
399
521
  resultElement.scrollTop = inputElement.scrollTop;
400
522
  resultElement.scrollLeft = inputElement.scrollLeft;
@@ -409,6 +531,15 @@ var codeInput = {
409
531
  return text.replace(new RegExp("&", "g"), "&amp;").replace(new RegExp("<", "g"), "&lt;"); /* Global RegExp */
410
532
  }
411
533
 
534
+ /**
535
+ * HTML-unescape an arbitrary string.
536
+ * @param {string} text - The original, HTML-escaped text
537
+ * @returns {string} - The new, unescaped text
538
+ */
539
+ unescapeHtml(text) {
540
+ return text.replace(new RegExp("&amp;", "g"), "&").replace(new RegExp("&lt;", "g"), "<").replace(new RegExp("&gt;", "g"), ">"); /* Global RegExp */
541
+ }
542
+
412
543
  /**
413
544
  * Get the template object this code-input element is using.
414
545
  * @returns {Object} - Template object
@@ -438,6 +569,8 @@ var codeInput = {
438
569
  * This will be called once the template has been added.
439
570
  */
440
571
  setup() {
572
+ if(this.textareaElement != null) return; // Already set up
573
+
441
574
  this.classList.add("code-input_registered"); // Remove register message
442
575
  if (this.template.preElementStyled) this.classList.add("code-input_pre-element-styled");
443
576
 
@@ -446,32 +579,52 @@ var codeInput = {
446
579
  // First-time attribute sync
447
580
  let lang = this.getAttribute("lang");
448
581
  let placeholder = this.getAttribute("placeholder") || this.getAttribute("lang") || "";
449
- let value = this.value || this.innerHTML || "";
582
+ let value = this.unescapeHtml(this.innerHTML) || this.getAttribute("value") || "";
583
+ // Value attribute deprecated, but included for compatibility
450
584
 
451
- this.innerHTML = ""; // Clear Content
585
+ this.initialValue = value; // For form reset
452
586
 
453
587
  // Create textarea
454
588
  let textarea = document.createElement("textarea");
455
589
  textarea.placeholder = placeholder;
456
- textarea.value = value;
590
+ if(value != "") {
591
+ textarea.value = value;
592
+ }
593
+ textarea.innerHTML = this.innerHTML;
457
594
  textarea.setAttribute("spellcheck", "false");
458
595
 
596
+ this.innerHTML = ""; // Clear Content
597
+
459
598
  // Synchronise attributes to textarea
460
599
  codeInput.textareaSyncAttributes.forEach((attribute) => {
461
600
  if (this.hasAttribute(attribute)) {
462
601
  textarea.setAttribute(attribute, this.getAttribute(attribute));
463
602
  }
464
603
  });
604
+ codeInput.textareaSyncAttributes.regexp.forEach((reg) =>
605
+ {
606
+ for(const attr of this.attributes) {
607
+ if (attr.nodeName.match(reg)) {
608
+ textarea.setAttribute(attr.nodeName, attr.nodeValue);
609
+ }
610
+ }
611
+ });
465
612
 
466
613
  textarea.addEventListener('input', (evt) => { textarea.parentElement.update(textarea.value); textarea.parentElement.sync_scroll(); });
467
614
  textarea.addEventListener('scroll', (evt) => textarea.parentElement.sync_scroll());
468
615
 
616
+ // Save element internally
617
+ this.textareaElement = textarea;
469
618
  this.append(textarea);
470
619
 
471
620
  // Create result element
472
621
  let code = document.createElement("code");
473
622
  let pre = document.createElement("pre");
474
623
  pre.setAttribute("aria-hidden", "true"); // Hide for screen readers
624
+
625
+ // Save elements internally
626
+ this.preElement = pre;
627
+ this.codeElement = code;
475
628
  pre.append(code);
476
629
  this.append(pre);
477
630
 
@@ -484,6 +637,8 @@ var codeInput = {
484
637
  this.pluginEvt("afterElementsAdded");
485
638
 
486
639
  this.update(value);
640
+
641
+ this.dispatchEvent(new CustomEvent("code-input_load"));
487
642
  }
488
643
 
489
644
  /**
@@ -520,20 +675,49 @@ var codeInput = {
520
675
  */
521
676
  connectedCallback() {
522
677
  this.template = this.getTemplate();
523
- if (this.template != undefined) this.setup();
678
+ if (this.template != undefined) {
679
+ this.classList.add("code-input_registered");
680
+ codeInput.runOnceWindowLoaded(() => {
681
+ this.setup();
682
+ this.classList.add("code-input_loaded");
683
+ }, this);
684
+ }
685
+ this.mutationObserver = new MutationObserver(this.mutationObserverCallback.bind(this));
686
+ this.mutationObserver.observe(this, {
687
+ attributes: true,
688
+ attributeOldValue: true
689
+ });
524
690
  }
525
691
 
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);
692
+ mutationObserverCallback(mutationList, observer) {
693
+ for (const mutation of mutationList) {
694
+ if (mutation.type !== 'attributes')
695
+ continue;
696
+
697
+ /* Check regular attributes */
698
+ for(let i = 0; i < codeInput.observedAttributes.length; i++) {
699
+ if (mutation.attributeName == codeInput.observedAttributes[i]) {
700
+ return this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, super.getAttribute(mutation.attributeName));
701
+ }
702
+ }
703
+
704
+ /* Check wildcard attributes */
705
+ for(let i = 0; i < codeInput.observedAttributes.regexp.length; i++) {
706
+ const reg = codeInput.observedAttributes.regexp[i];
707
+ if (mutation.attributeName.match(reg)) {
708
+ return this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, super.getAttribute(mutation.attributeName));
709
+ }
710
+ }
711
+ }
712
+ }
713
+
714
+ disconnectedCallback() {
715
+ this.mutationObserver.disconnect();
532
716
  }
533
717
 
534
718
  /**
535
- * Triggered when an HTML attribute in `codeInput.CodeInput.observedAttributes`
536
- * has been modified.
719
+ * Triggered when an observed HTML attribute
720
+ * has been modified (called from `mutationObserverCallback`).
537
721
  * @param {string} name - The name of the attribute
538
722
  * @param {string} oldValue - The value of the attribute before it was changed
539
723
  * @param {string} newValue - The value of the attribute after it is changed
@@ -544,10 +728,10 @@ var codeInput = {
544
728
  switch (name) {
545
729
 
546
730
  case "value":
547
- this.update(newValue);
731
+ this.value = newValue;
548
732
  break;
549
733
  case "placeholder":
550
- this.querySelector("textarea").placeholder = newValue;
734
+ this.textareaElement.placeholder = newValue;
551
735
  break;
552
736
  case "template":
553
737
  this.template = codeInput.usedTemplates[newValue || codeInput.defaultTemplate];
@@ -560,8 +744,8 @@ var codeInput = {
560
744
 
561
745
  case "lang":
562
746
 
563
- let code = this.querySelector("pre code");
564
- let mainTextarea = this.querySelector("textarea");
747
+ let code = this.codeElement;
748
+ let mainTextarea = this.textareaElement;
565
749
 
566
750
  // Check not already updated
567
751
  if (newValue != null) {
@@ -576,14 +760,14 @@ var codeInput = {
576
760
 
577
761
  // Remove old language class and add new
578
762
  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
763
+ code.classList.remove("language-" + oldValue); // From codeElement
764
+ code.parentElement.classList.remove("language-" + oldValue); // From preElement
581
765
  code.classList.remove("language-none"); // Prism
582
766
  code.parentElement.classList.remove("language-none"); // Prism
583
767
 
584
768
  if (newValue != undefined && newValue != "") {
585
769
  code.classList.add("language-" + newValue);
586
- console.log("code-input: Language:ADD", "language-" + newValue);
770
+ console.log("code-input: Language: ADD", "language-" + newValue);
587
771
  }
588
772
 
589
773
  if (mainTextarea.placeholder == oldValue) mainTextarea.placeholder = newValue;
@@ -593,7 +777,21 @@ var codeInput = {
593
777
  break;
594
778
  default:
595
779
  if (codeInput.textareaSyncAttributes.includes(name)) {
596
- this.querySelector("textarea").setAttribute(name, newValue);
780
+ if(newValue == null) {
781
+ this.textareaElement.removeAttribute(name);
782
+ } else {
783
+ this.textareaElement.setAttribute(name, newValue);
784
+ }
785
+ } else {
786
+ codeInput.textareaSyncAttributes.regexp.forEach((attribute) => {
787
+ if (name.match(attribute)) {
788
+ if(newValue == null) {
789
+ this.textareaElement.removeAttribute(name);
790
+ } else {
791
+ this.textareaElement.setAttribute(name, newValue);
792
+ }
793
+ }
794
+ });
597
795
  }
598
796
  break;
599
797
  }
@@ -616,9 +814,17 @@ var codeInput = {
616
814
 
617
815
  if (codeInput.textareaSyncEvents.includes(type)) {
618
816
  if (options === undefined) {
619
- this.querySelector("textarea").addEventListener(type, boundCallback);
817
+ if(this.textareaElement == null) {
818
+ this.addEventListener("code-input_load", () => { this.textareaElement.addEventListener(type, boundCallback); });
819
+ } else {
820
+ this.textareaElement.addEventListener(type, boundCallback);
821
+ }
620
822
  } else {
621
- this.querySelector("textarea").addEventListener(type, boundCallback, options);
823
+ if(this.textareaElement == null) {
824
+ this.addEventListener("code-input_load", () => { this.textareaElement.addEventListener(type, boundCallback, options); });
825
+ } else {
826
+ this.textareaElement.addEventListener(type, boundCallback, options);
827
+ }
622
828
  }
623
829
  } else {
624
830
  if (options === undefined) {
@@ -636,15 +842,15 @@ var codeInput = {
636
842
  let boundCallback = this.boundEventCallbacks[listener];
637
843
  if (type == "change") {
638
844
  if (options === null) {
639
- this.querySelector("textarea").removeEventListener("change", boundCallback);
845
+ this.textareaElement.removeEventListener("change", boundCallback);
640
846
  } else {
641
- this.querySelector("textarea").removeEventListener("change", boundCallback, options);
847
+ this.textareaElement.removeEventListener("change", boundCallback, options);
642
848
  }
643
849
  } else if (type == "selectionchange") {
644
850
  if (options === null) {
645
- this.querySelector("textarea").removeEventListener("selectionchange", boundCallback);
851
+ this.textareaElement.removeEventListener("selectionchange", boundCallback);
646
852
  } else {
647
- this.querySelector("textarea").removeEventListener("selectionchange", boundCallback, options);
853
+ this.textareaElement.removeEventListener("selectionchange", boundCallback, options);
648
854
  }
649
855
  } else {
650
856
  super.removeEventListener(type, listener, options);
@@ -655,14 +861,19 @@ var codeInput = {
655
861
  * Get the text contents of the code-input element.
656
862
  */
657
863
  get value() {
658
- return this.getAttribute("value");
864
+ return this._value;
659
865
  }
660
866
  /**
661
867
  * Set the text contents of the code-input element.
662
868
  * @param {string} val - New text contents
663
869
  */
664
870
  set value(val) {
665
- return this.setAttribute("value", val);
871
+ if (val === null || val === undefined) {
872
+ val = "";
873
+ }
874
+ this._value = val;
875
+ this.update(val);
876
+ return val;
666
877
  }
667
878
 
668
879
  /**
@@ -687,7 +898,7 @@ var codeInput = {
687
898
  * See `HTMLTextAreaElement.validity`
688
899
  */
689
900
  get validity() {
690
- return this.querySelector("textarea").validity;
901
+ return this.textareaElement.validity;
691
902
  }
692
903
 
693
904
  /**
@@ -698,7 +909,7 @@ var codeInput = {
698
909
  * See `HTMLTextAreaElement.validationMessage`
699
910
  */
700
911
  get validationMessage() {
701
- return this.querySelector("textarea").validationMessage;
912
+ return this.textareaElement.validationMessage;
702
913
  }
703
914
 
704
915
  /**
@@ -708,7 +919,7 @@ var codeInput = {
708
919
  * @param error Sets a custom error message that is displayed when a form is submitted.
709
920
  */
710
921
  setCustomValidity(error) {
711
- return this.querySelector("textarea").setCustomValidity(error);
922
+ return this.textareaElement.setCustomValidity(error);
712
923
  }
713
924
 
714
925
  /**
@@ -718,14 +929,14 @@ var codeInput = {
718
929
  * See `HTMLTextAreaElement.checkValidity`
719
930
  */
720
931
  checkValidity() {
721
- return this.querySelector("textarea").checkValidity();
932
+ return this.textareaElement.checkValidity();
722
933
  }
723
934
 
724
935
  /**
725
936
  * See `HTMLTextAreaElement.reportValidity`
726
937
  */
727
938
  reportValidity() {
728
- return this.querySelector("textarea").reportValidity();
939
+ return this.textareaElement.reportValidity();
729
940
  }
730
941
 
731
942
 
@@ -734,17 +945,19 @@ var codeInput = {
734
945
  */
735
946
  setAttribute(qualifiedName, value) {
736
947
  super.setAttribute(qualifiedName, value); // code-input
737
- this.querySelector("textarea").setAttribute(qualifiedName, value); // textarea
948
+ if(this.textareaElement != null) {
949
+ this.textareaElement.setAttribute(qualifiedName, value); // textarea
950
+ }
738
951
  }
739
952
 
740
953
  /**
741
954
  * @override
742
955
  */
743
956
  getAttribute(qualifiedName) {
744
- if (this.querySelector("textarea") == null) {
957
+ if (this.textareaElement == null) {
745
958
  return super.getAttribute(qualifiedName);
746
959
  }
747
- return this.querySelector("textarea").getAttribute(qualifiedName); // textarea
960
+ return this.textareaElement.getAttribute(qualifiedName); // textarea
748
961
  }
749
962
 
750
963
  /**
@@ -753,7 +966,79 @@ var codeInput = {
753
966
  * Value - object of data to be stored; different plugins may use this differently.
754
967
  */
755
968
  pluginData = {};
756
- }
969
+
970
+ /**
971
+ * Update value on form reset
972
+ */
973
+ formResetCallback() {
974
+ this.update(this.initialValue);
975
+ };
976
+ },
977
+
978
+ arrayWildcards2regex(list) {
979
+ for(let i = 0; i < list.length; i++) {
980
+ const name = list[i];
981
+ if (name.indexOf("*") < 0)
982
+ continue;
983
+
984
+ list.regexp.push(new RegExp("^" +
985
+ name.replace(/[/\-\\^$+?.()|[\]{}]/g, '\\$&')
986
+ .replace("*", ".*")
987
+ + "$", "i"));
988
+ list.splice(i--, 1);
989
+ };
990
+ },
991
+
992
+ wildcard2regex(wildcard) {
993
+ if (wildcard.indexOf("*") < 0)
994
+ return null;
995
+
996
+ return new RegExp("^" +
997
+ wildcard.replace(/[/\-\\^$+?.()|[\]{}]/g, '\\$&')
998
+ .replace("*", ".*")
999
+ + "$", "i");
1000
+ },
1001
+
1002
+ /**
1003
+ * To ensure the DOM is ready, run this callback after the window
1004
+ * has loaded (or now if it has already loaded)
1005
+ */
1006
+ runOnceWindowLoaded(callback, codeInputElem) {
1007
+ if(codeInput.windowLoaded) {
1008
+ callback(); // Fully loaded
1009
+ } else {
1010
+ window.addEventListener("load", callback);
1011
+ }
1012
+ },
1013
+ windowLoaded: false
1014
+ }
1015
+ window.addEventListener("load", function() {
1016
+ codeInput.windowLoaded = true;
1017
+ });
1018
+
1019
+
1020
+ /**
1021
+ * convert wildcards into regex
1022
+ */
1023
+
1024
+ {
1025
+ Object.defineProperty(codeInput.textareaSyncAttributes, 'regexp', {
1026
+ value: [],
1027
+ writable: false,
1028
+ enumerable: false,
1029
+ configurable: false
1030
+ });
1031
+ codeInput.observedAttributes = codeInput.observedAttributes.concat(codeInput.textareaSyncAttributes);
1032
+
1033
+ Object.defineProperty(codeInput.observedAttributes, 'regexp', {
1034
+ value: [],
1035
+ writable: false,
1036
+ enumerable: false,
1037
+ configurable: false
1038
+ });
1039
+
1040
+ codeInput.arrayWildcards2regex(codeInput.textareaSyncAttributes);
1041
+ codeInput.arrayWildcards2regex(codeInput.observedAttributes);
757
1042
  }
758
1043
 
759
- customElements.define("code-input", codeInput.CodeInput);
1044
+ customElements.define("code-input", codeInput.CodeInput);