diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52f8649..ce82c71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,15 @@ * The `code-input` element should be easy to use with all popular syntax-highlighting libraries. * Any modifications of `code-input` that would be useful for the open-source community but are not core to this functionality should be available as optional plugins in the `plugins` folder. Here's where most feature contributions will go. -To keep this community productive and enjoyable, please [don't break the code of conduct here](https://github.com/WebCoder49/code-input/blob/main/CODE_OF_CONDUCT.md). +We will generally *not* consider the following contributions: +* Excess functionality and/or complexity in the main code-input files - these types of contributions should go in the plugin folder instead. +* Issues that have been closed as not planned in the past (you can search the issue list to check), unless you bring a change that overcomes the reason they were not planned. + +This said, if you're not sure whether your change will be accepted, please ask in an issue. + +--- + +To keep this community productive and enjoyable, please [don't break our code of conduct](https://github.com/WebCoder49/code-input/blob/main/CODE_OF_CONDUCT.md). --- # Ways you could contribute: @@ -22,4 +30,6 @@ Firstly, thank you for doing this! This is probably the most time consuming way Please first open an issue if one doesn't already exist, and assign yourself to it. Then, [make a fork of the repo and submit a pull request](https://docs.github.com/en/get-started/quickstart/contributing-to-projects). +In the pull request, include the code updates for your feature / bug, and if you're adding a new feature make sure you comment your code so it's understandable to future contributors, and if you can, add unit tests for it in tests/tester.js. If you have any questions, just let me (@WebCoder49) know! + If an issue is open but already assigned to someone, it is probably already being worked on - you could still suggest a method of fixing it in the comments but shouldn't open a pull request as it would waste your time. diff --git a/README.md b/README.md index 7d24f7d..e0ce77f 100644 --- a/README.md +++ b/README.md @@ -88,11 +88,6 @@ The next step is to set up a `template` to link `code-input` to your syntax-high * argument to the highlight function to be used for getting data- attribute values * and using the DOM for the code-input */, - true /* Optional - Leaving this as true uses code-input's default fix for preventing duplicate - * results in Ctrl+F searching from the input and result elements, and setting this to false - * indicates your highlighting function implements its own fix. The default fix works by moving - * text content from elements to CSS ::before pseudo-elements after highlighting. */ - [] // Array of plugins (see below) )); ``` diff --git a/code-input.css b/code-input.css index ef1b23c..97a85bf 100644 --- a/code-input.css +++ b/code-input.css @@ -4,42 +4,54 @@ code-input { - /* Allow other elems to be inside */ + /* Allow other elements to be inside */ + display: block; + overflow-y: auto; + overflow-x: auto; position: relative; top: 0; left: 0; - display: block; - /* Only scroll inside elems */ - overflow: hidden; /* Normal inline styles */ margin: 8px; --padding: 16px; height: 250px; - - font-size: normal; + font-size: inherit; font-family: monospace; line-height: 1.5; /* Inherited to child elements */ tab-size: 2; caret-color: darkgrey; white-space: pre; - padding: 0!important; /* Use --padding */ + padding: 0!important; /* Use --padding to set the code-input element's padding */ + display: grid; + grid-template-columns: 100%; + grid-template-rows: 100%; } -code-input textarea, code-input:not(.code-input_pre-element-styled) pre code, code-input.code-input_pre-element-styled pre { - /* Both elements need the same text and space styling so they are directly on top of each other */ + +code-input:not(.code-input_loaded) { margin: 0px!important; + margin-bottom: calc(-1 * var(--padding, 16px))!important; padding: var(--padding, 16px)!important; border: 0; - width: calc(100% - var(--padding, 16px) * 2); - height: calc(100% - var(--padding, 16px) * 2); } -code-input:not(.code-input_loaded) { +code-input textarea, code-input:not(.code-input_pre-element-styled) pre code, code-input.code-input_pre-element-styled pre { + /* Both elements need the same text and space styling so they are directly on top of each other */ margin: 0px!important; - margin-bottom: calc(-1 * var(--padding, 16px))!important; padding: var(--padding, 16px)!important; border: 0; + min-width: calc(100% - var(--padding, 16px) * 2); + min-height: calc(100% - var(--padding, 16px) * 2); + overflow: hidden; + resize: none; + grid-row: 1; + grid-column: 1; + display: block; +} +code-input:not(.code-input_pre-element-styled) pre code, code-input.code-input_pre-element-styled pre { + height: max-content; + width: max-content; } code-input:not(.code-input_pre-element-styled) pre, code-input.code-input_pre-element-styled pre code { @@ -58,15 +70,12 @@ code-input textarea, code-input pre, code-input pre * { tab-size: inherit!important; } - code-input textarea, code-input pre { /* In the same place */ - position: absolute; - top: 0; - left: 0; + grid-column: 1; + grid-row: 1; } - /* Move the textarea in front of the result */ code-input textarea { @@ -79,7 +88,7 @@ code-input pre { code-input:not(.code-input_loaded) pre, code-input:not(.code-input_loaded) textarea { opacity: 0; } -code-input:not(.code-input_loaded)::after { +code-input:not(.code-input_loaded)::before { color: #ccc; } @@ -96,8 +105,6 @@ code-input textarea::placeholder { /* Can be scrolled */ code-input textarea, code-input pre { - overflow: auto!important; - white-space: inherit; word-spacing: normal; word-break: normal; @@ -110,17 +117,9 @@ code-input textarea { outline: none!important; } -code-input:not(.code-input_registered)::after { +code-input:not(.code-input_registered)::before { /* Display message to register */ content: "Use codeInput.registerTemplate to set up."; display: block; color: grey; -} - -/* To prevent Ctrl+F in result element */ -code-input pre .code-input_searching-disabled::before { - content: attr(data-content); -} -code-input pre .code-input_searching-disabled { - font-size: 0; } \ No newline at end of file diff --git a/code-input.d.ts b/code-input.d.ts index 22b8eb9..52582a1 100644 --- a/code-input.d.ts +++ b/code-input.d.ts @@ -95,7 +95,7 @@ export namespace plugins { */ class Autocomplete extends Plugin { /** - * Pass in a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd). + * Pass in a function to create a plugin that displays the popup that takes in (popup element, textarea, textarea.selectionEnd). * @param {function} updatePopupCallback a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd). */ constructor(updatePopupCallback: (popupElem: HTMLElement, textarea: HTMLTextAreaElement, selectionEnd: number) => void); diff --git a/code-input.js b/code-input.js index 987f54e..7142d4e 100644 --- a/code-input.js +++ b/code-input.js @@ -19,6 +19,7 @@ var codeInput = { observedAttributes: [ "value", "placeholder", + "language", "lang", "template" ], @@ -29,7 +30,6 @@ var codeInput = { * code-input element. */ textareaSyncAttributes: [ - "aria-*", "value", // Form validation - https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation#using_built-in_form_validation "min", "max", @@ -108,7 +108,6 @@ var codeInput = { 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.`); 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.`); 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.`); - if(!(typeof template.autoDisableDuplicateSearching == "boolean" || template.autoDisableDuplicateSearching instanceof Boolean)) throw TypeError(`code-input: Template for "${templateName}" invalid, because the autoDisableDuplicateSearching value provided is not a true or false; it is "${template.autoDisableDuplicateSearching}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`); 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.`); template.plugins.forEach((plugin, i) => { @@ -125,7 +124,7 @@ var codeInput = { elem = codeInput.templateNotYetRegisteredQueue[templateName][i]; elem.template = template; codeInput.runOnceWindowLoaded((function(elem) { elem.connectedCallback(); }).bind(null, elem), elem); - // Bind sets elem in parameter + // Bind sets elem as first parameter of function // So innerHTML can be read } console.log(`code-input: template: Added existing elements with template ${templateName}`); @@ -139,7 +138,7 @@ var codeInput = { elem = codeInput.templateNotYetRegisteredQueue[undefined][i]; elem.template = template; codeInput.runOnceWindowLoaded((function(elem) { elem.connectedCallback(); }).bind(null, elem), elem); - // Bind sets elem in parameter + // Bind sets elem as first parameter of function // So innerHTML can be read } } @@ -166,18 +165,11 @@ var codeInput = { * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.Plugin` * @returns {codeInput.Template} template object */ - constructor(highlight = function () { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, autoDisableDuplicateSearching = true, plugins = []) { - // @deprecated to support old function signature without autoDisableDuplicateSearching - if(Array.isArray(autoDisableDuplicateSearching)) { - plugins = autoDisableDuplicateSearching; - autoDisableDuplicateSearching = true; - } - + constructor(highlight = function () { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, plugins = []) { this.highlight = highlight; this.preElementStyled = preElementStyled; this.isCode = isCode; this.includeCodeInputInHighlightFunc = includeCodeInputInHighlightFunc; - this.autoDisableDuplicateSearching = autoDisableDuplicateSearching; this.plugins = plugins; } @@ -210,15 +202,6 @@ var codeInput = { */ includeCodeInputInHighlightFunc = false; - /** - * Leaving this as true uses code-input's default fix for preventing duplicate results in Ctrl+F searching - * from the input and result elements, and setting this to false indicates your highlighting function implements - * its own fix. - * - * The default fix works by moving text content from elements to CSS ::before pseudo-elements after highlighting. - */ - autoDisableDuplicateSearching = true; - /** * An array of plugin objects to add extra features - * see `codeInput.Plugin`. @@ -251,7 +234,6 @@ var codeInput = { true, // preElementStyled true, // isCode false, // includeCodeInputInHighlightFunc - true, // autoDisableDuplicateSearching plugins ); }, @@ -270,15 +252,12 @@ var codeInput = { false, // preElementStyled true, // isCode false, // includeCodeInputInHighlightFunc - true, // autoDisableDuplicateSearching plugins ); }, /** - * Constructor to create a proof-of-concept template that gives a message if too many characters are typed. - * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins` - * @returns template object + * @deprecated Make your own version of this template if you need it - we think it isn't widely used so will remove it from the next version of code-input. */ characterLimit(plugins) { return { @@ -297,17 +276,12 @@ var codeInput = { includeCodeInputInHighlightFunc: true, preElementStyled: true, isCode: false, - autoDisableDuplicateSearching: true, plugins: plugins, } }, /** - * Constructor to create a proof-of-concept template that shows text in a repeating series of colors. - * @param {string[]} rainbowColors - An array of CSS colors, in the order each color will be shown - * @param {string} delimiter - The character used to split up parts of text where each part is a different colour (e.g. "" = characters, " " = words) - * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins` - * @returns template object + * @deprecated Make your own version of this template if you need it - we think it isn't widely used so will remove it from the next version of code-input. */ rainbowText(rainbowColors = ["red", "orangered", "orange", "goldenrod", "gold", "green", "darkgreen", "navy", "blue", "magenta"], delimiter = "", plugins = []) { return { @@ -322,7 +296,6 @@ var codeInput = { includeCodeInputInHighlightFunc: true, preElementStyled: true, isCode: false, - autoDisableDuplicateSearching: true, rainbowColors: rainbowColors, delimiter: delimiter, @@ -332,13 +305,13 @@ var codeInput = { }, /** - * @deprecated Please use `codeInput.characterLimit(plugins)` + * @deprecated Make your own version of this template if you need it - we think it isn't widely used so will remove it from the next version of code-input. */ character_limit() { return this.characterLimit([]); }, /** - * @deprecated Please use `codeInput.rainbowText` + * @deprecated Make your own version of this template if you need it - we think it isn't widely used so will remove it from the next version of code-input. */ rainbow_text(rainbowColors = ["red", "orangered", "orange", "goldenrod", "gold", "green", "darkgreen", "navy", "blue", "magenta"], delimiter = "", plugins = []) { return this.rainbowText(rainbowColors, delimiter, plugins); @@ -400,14 +373,7 @@ var codeInput = { console.log("code-input: plugin: Created plugin"); observedAttributes.forEach((attribute) => { - // Move plugin attribute to codeInput observed attributes - let regexFromWildcard = codeInput.wildcard2regex(attribute); - if(regexFromWildcard == null) { - // Not a wildcard - codeInput.observedAttributes.push(attribute); - } else { - codeInput.observedAttributes.regexp.push(regexFromWildcard); - } + codeInput.observedAttributes.push(attribute); }); } @@ -452,10 +418,6 @@ var codeInput = { constructor() { super(); // Element } - /** - * Store value internally - */ - _value = ''; /** * Exposed child textarea element for user to input code in @@ -513,30 +475,47 @@ var codeInput = { * the result (pre code) element, then use the template object * to syntax-highlight it. */ - /** Update the text value to the result element, after the textarea contents have changed. - * @param {string} value - The text value of the code-input element - * @param {boolean} originalUpdate - Whether this update originates from the textarea's content; if so, run it first so custom updates override it. + needsHighlight = false; // Just inputted + + /** + * Highlight the code as soon as possible + */ + scheduleHighlight() { + this.needsHighlight = true; + } + + /** + * Call an animation frame */ - update(value) { - // Prevent this from running multiple times on the same input when "value" attribute is changed, - // by not running when value is already equal to the input of this (implying update has already - // been run). Thank you to peterprvy for this. - if (this.ignoreValueUpdate) return; - - if(this.textareaElement == null) { - this.addEventListener("code-input_load", () => { this.update(value) }); // Only run when fully loaded - return; + animateFrame() { + // Synchronise the size of the pre/code and textarea elements + if(this.template.preElementStyled) { + this.style.backgroundColor = getComputedStyle(this.preElement).backgroundColor; + this.textareaElement.style.height = getComputedStyle(this.preElement).height; + this.textareaElement.style.width = getComputedStyle(this.preElement).width; + } else { + this.style.backgroundColor = getComputedStyle(this.codeElement).backgroundColor; + this.textareaElement.style.height = getComputedStyle(this.codeElement).height; + this.textareaElement.style.width = getComputedStyle(this.codeElement).width; } - this.ignoreValueUpdate = true; - this.value = value; - this.ignoreValueUpdate = false; - if (this.textareaElement.value != value) this.textareaElement.value = value; + // Synchronise the contents of the pre/code and textarea elements + if(this.needsHighlight) { + this.update(); + this.needsHighlight = false; + } + window.requestAnimationFrame(this.animateFrame.bind(this)); + } + /** + * Update the text value to the result element, after the textarea contents have changed. + */ + update() { let resultElement = this.codeElement; + let value = this.value; // Handle final newlines - if (value[value.length - 1] == "\n") { + if (value[value.length - 1] == "\n" || value.length == 0) { value += " "; } @@ -549,37 +528,6 @@ var codeInput = { else this.template.highlight(resultElement); this.pluginEvt("afterHighlight"); - - if(this.template.autoDisableDuplicateSearching) { - if(this.codeElement.querySelector("*") === null) { - // Fix for tries-to-disable-searching-before-highlighting-possible bug: - // Wait until user interaction so can expect - // highlight before disabling searching - let listenerKeydown = window.addEventListener("keydown", () => { - this.resultElementDisableSearching(); - window.removeEventListener("keydown", listenerKeydown); - window.removeEventListener("mousemove", listenerMousemove); - }); - let listenerMousemove = window.addEventListener("mousemove", () => { - this.resultElementDisableSearching(); - window.removeEventListener("keydown", listenerKeydown); - window.removeEventListener("mousemove", listenerMousemove); - }); - } else { - this.resultElementDisableSearching(); - } - } - } - - /** - * Synchronise the scrolling of the textarea to the result element. - */ - syncScroll() { - let inputElement = this.textareaElement; - let resultElement = this.template.preElementStyled ? this.preElement : this.codeElement; - - resultElement.scrollTop = inputElement.scrollTop; - resultElement.scrollLeft = inputElement.scrollLeft; } /** @@ -600,30 +548,6 @@ var codeInput = { return text.replace(new RegExp("&", "g"), "&").replace(new RegExp("<", "g"), "<").replace(new RegExp(">", "g"), ">"); /* Global RegExp */ } - /** - * Make the text contents of highlighted code in the `
` result element invisible to Ctrl+F by moving them to a data attribute
-         * then the CSS `::before` pseudo-element on span elements with the class code-input_searching-disabled. This function is called recursively
-         * on all child elements of the  element.
-         * 
-         * @param {HTMLElement} element The element on which this is carried out recursively. Optional - defaults to the `
`'s `` element. 
-         */
-        resultElementDisableSearching(element=this.preElement) {
-            for (let i = 0; i < element.childNodes.length; i++) {
-                let content = element.childNodes[i].textContent;
-            
-                if (element.childNodes[i].nodeType == 3) {
-                    // Turn plain text node into span element
-                    element.replaceChild(document.createElement('span'), element.childNodes[i]);
-                    element.childNodes[i].classList.add("code-input_searching-disabled")
-                    element.childNodes[i].setAttribute("data-content", content);
-                    element.childNodes[i].innerText = '';
-                } else {
-                    // Recurse deeper
-                    this.resultElementDisableSearching(element.childNodes[i]);
-                }
-            }
-        }
-
         /**
          * Get the template object this code-input element is using.
          * @returns {Object} - Template object
@@ -661,7 +585,7 @@ var codeInput = {
             this.pluginEvt("beforeElementsAdded");
 
             // First-time attribute sync
-            let lang = this.getAttribute("lang");
+            let lang = this.getAttribute("language") || this.getAttribute("lang");
             let placeholder = this.getAttribute("placeholder") || this.getAttribute("lang") || "";
             let value = this.unescapeHtml(this.innerHTML) || this.getAttribute("value") || "";
             // Value attribute deprecated, but included for compatibility
@@ -680,22 +604,14 @@ var codeInput = {
             this.innerHTML = ""; // Clear Content
 
             // Synchronise attributes to textarea
-            codeInput.textareaSyncAttributes.forEach((attribute) => {
-                if (this.hasAttribute(attribute)) {
+            for(let i = 0; i < this.attributes.length; i++) {
+                let attribute = this.attributes[i].name;
+                if (codeInput.textareaSyncAttributes.includes(attribute) || attribute.substring(0, 5) == "aria-") {
                     textarea.setAttribute(attribute, this.getAttribute(attribute));
                 }
-            });
-            codeInput.textareaSyncAttributes.regexp.forEach((reg) =>
-            {
-                for(const attr of this.attributes) {
-                    if (attr.nodeName.match(reg)) {
-                        textarea.setAttribute(attr.nodeName, attr.nodeValue);
-                    }
-                }
-            });
+            }
 
-            textarea.addEventListener('input', (evt) => { textarea.parentElement.update(textarea.value); textarea.parentElement.sync_scroll(); });
-            textarea.addEventListener('scroll', (evt) => textarea.parentElement.sync_scroll());
+            textarea.addEventListener('input', (evt) => { this.value = this.textareaElement.value; });
 
             // Save element internally
             this.textareaElement = textarea;
@@ -714,22 +630,16 @@ var codeInput = {
 
             if (this.template.isCode) {
                 if (lang != undefined && lang != "") {
-                    code.classList.add("language-" + lang);
+                    code.classList.add("language-" + lang.toLowerCase());
                 }
             }
 
             this.pluginEvt("afterElementsAdded");
 
-            this.update(value);
-
             this.dispatchEvent(new CustomEvent("code-input_load"));
-        }
 
-        /**
-         * @deprecated Please use `codeInput.CodeInput.syncScroll`
-         */
-        sync_scroll() {
-            this.syncScroll();
+            this.value = value;
+            this.animateFrame();
         }
 
         /**
@@ -740,7 +650,7 @@ var codeInput = {
         }
 
         /**
-         * @deprecated Please use `codeInput.CodeInput.escapeHtml`
+         * @deprecated Please use `codeInput.CodeInput.getTemplate`
          */
         get_template() {
             return this.getTemplate();
@@ -784,13 +694,8 @@ var codeInput = {
                         return this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, super.getAttribute(mutation.attributeName));
                     }
                 }
-
-                /* Check wildcard attributes */
-                for(let i = 0; i < codeInput.observedAttributes.regexp.length; i++) {
-                    const reg = codeInput.observedAttributes.regexp[i];
-                    if (mutation.attributeName.match(reg)) {
-                        return this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, super.getAttribute(mutation.attributeName));
-                    }
+                if (mutation.attributeName.substring(0, 5) == "aria-") {
+                    return this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, super.getAttribute(mutation.attributeName));
                 }
             }
         }
@@ -814,20 +719,17 @@ var codeInput = {
                     case "value":
                         this.value = newValue;
                         break;
-                    case "placeholder":
-                        this.textareaElement.placeholder = newValue;
-                        break;
                     case "template":
                         this.template = codeInput.usedTemplates[newValue || codeInput.defaultTemplate];
                         if (this.template.preElementStyled) this.classList.add("code-input_pre-element-styled");
                         else this.classList.remove("code-input_pre-element-styled");
                         // Syntax Highlight
-                        this.update(this.value);
+                        this.needsHighlight = true;
 
                         break;
 
                     case "lang":
-
+                    case "language":
                         let code = this.codeElement;
                         let mainTextarea = this.textareaElement;
 
@@ -854,11 +756,11 @@ var codeInput = {
 
                         if (mainTextarea.placeholder == oldValue) mainTextarea.placeholder = newValue;
 
-                        this.update(this.value);
+                        this.needsHighlight = true;
 
                         break;
                     default:
-                        if (codeInput.textareaSyncAttributes.includes(name)) {
+                        if (codeInput.textareaSyncAttributes.includes(name) || name.substring(0, 5) == "aria-") {
                             if(newValue == null || newValue == undefined) {
                                 this.textareaElement.removeAttribute(name);
                             } else {
@@ -943,7 +845,8 @@ var codeInput = {
          * Get the text contents of the code-input element.
          */
         get value() {
-            return this._value;
+            // Get from editable textarea element
+            return this.textareaElement.value;
         }
         /**
          * Set the text contents of the code-input element.
@@ -953,8 +856,10 @@ var codeInput = {
             if (val === null || val === undefined) {
                 val = "";
             }
-            this._value = val;
-            this.update(val);
+            // Save in editable textarea element
+            this.textareaElement.value = val;
+            // Trigger highlight
+            this.needsHighlight = true;
             return val;
         }
 
@@ -1032,34 +937,10 @@ var codeInput = {
         * Update value on form reset
         */
         formResetCallback() {
-            this.update(this.initialValue);
+            this.value = this.initialValue;
         };
     },
 
-    arrayWildcards2regex(list) {
-        for(let i = 0; i < list.length; i++) {
-            const name = list[i];
-            if (name.indexOf("*") < 0)
-                continue;
-
-            list.regexp.push(new RegExp("^" +
-                                name.replace(/[/\-\\^$+?.()|[\]{}]/g, '\\$&')
-                                    .replace("*", ".*")
-                                + "$", "i"));
-            list.splice(i--, 1);
-        };
-    },
-
-    wildcard2regex(wildcard) {
-        if (wildcard.indexOf("*") < 0)
-            return null;
-
-        return new RegExp("^" +
-                wildcard.replace(/[/\-\\^$+?.()|[\]{}]/g, '\\$&')
-                    .replace("*", ".*")
-                + "$", "i");
-    },
-
     /** 
      * To ensure the DOM is ready, run this callback after the window 
      * has loaded (or now if it has already loaded)
@@ -1077,29 +958,4 @@ window.addEventListener("load", function() {
     codeInput.windowLoaded = true;
 });
 
-
-/**
- * convert wildcards into regex
- */
-
-{
-    Object.defineProperty(codeInput.textareaSyncAttributes, 'regexp', {
-        value: [],
-        writable: false,
-        enumerable: false,
-        configurable: false
-    });
-    codeInput.observedAttributes = codeInput.observedAttributes.concat(codeInput.textareaSyncAttributes);
-
-    Object.defineProperty(codeInput.observedAttributes, 'regexp', {
-        value: [],
-        writable: false,
-        enumerable: false,
-        configurable: false
-    });
-
-    codeInput.arrayWildcards2regex(codeInput.textareaSyncAttributes);
-    codeInput.arrayWildcards2regex(codeInput.observedAttributes);
-}
-
-customElements.define("code-input", codeInput.CodeInput);
+customElements.define("code-input", codeInput.CodeInput);
\ No newline at end of file
diff --git a/plugins/README.md b/plugins/README.md
index 9c27dd8..5ccb55e 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -29,13 +29,6 @@ Files: [autodetect.js](./autodetect.js)
 
 [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/eYLyMae)
 
-### Debounce Update
-Debounce the update and highlighting function ([What is Debouncing?](https://medium.com/@jamischarles/what-is-debouncing-2505c0648ff1))
-
-Files: [debounce-update.js](./debounce-update.js)
-
-[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/GRXyxzV)
-
 ### Go To Line
 Add a feature to go to a specific line when a line number is given (or column as well, in the format line no:column no) that appears when (optionally) Ctrl+G is pressed or when JavaScript triggers it.
 
diff --git a/plugins/auto-close-brackets.js b/plugins/auto-close-brackets.js
index 54e20f5..a30a813 100644
--- a/plugins/auto-close-brackets.js
+++ b/plugins/auto-close-brackets.js
@@ -19,35 +19,42 @@ codeInput.plugins.AutoCloseBrackets = class extends codeInput.Plugin {
 
     /* Add keystroke events */
     afterElementsAdded(codeInput) {
-        let textarea = codeInput.textareaElement;
-        textarea.addEventListener('keydown', (event) => { this.checkBackspace(codeInput, event) });
-        textarea.addEventListener('beforeinput', (event) => { this.checkBrackets(codeInput, event); });
-
+        codeInput.textareaElement.addEventListener('keydown', (event) => { this.checkBackspace(codeInput, event) });
+        codeInput.textareaElement.addEventListener('beforeinput', (event) => { this.checkBrackets(codeInput, event); });
     }
 
-    /* Event handlers */
+    /* Deal with the automatic creation of closing bracket when opening brackets are typed, and the ability to "retype" a closing
+    bracket where one has already been placed. */
     checkBrackets(codeInput, event) {
-        if(this.bracketsOpenedStack.length > 0 && event.data == this.bracketsOpenedStack[this.bracketsOpenedStack.length-1][0] && event.data == codeInput.textareaElement.value[codeInput.textareaElement.selectionStart]) {
-            // "Retype" bracket, i.e. just move caret
-            codeInput.textareaElement.selectionStart = codeInput.textareaElement.selectionEnd += 1;
-            this.bracketsOpenedStack.pop();
-            event.preventDefault();
+        if(event.data == codeInput.textareaElement.value[codeInput.textareaElement.selectionStart]) {
+            // Check if a closing bracket is typed
+            for(let openingBracket in this.bracketPairs) {
+                let closingBracket = this.bracketPairs[openingBracket];
+                if(event.data == closingBracket) {
+                    // "Retype" a closing bracket, i.e. just move caret
+                    codeInput.textareaElement.selectionStart = codeInput.textareaElement.selectionEnd += 1;
+                    event.preventDefault();
+                    break;
+                }
+            }
         } else if(event.data in this.bracketPairs) {
-            // Create bracket pair
+            // Opening bracket typed; Create bracket pair
             let closingBracket = this.bracketPairs[event.data];
-            this.bracketsOpenedStack.push([closingBracket, codeInput.textareaElement.selectionStart]);
+            // Insert the closing bracket
             document.execCommand("insertText", false, closingBracket);
+            // Move caret before the inserted closing bracket
             codeInput.textareaElement.selectionStart = codeInput.textareaElement.selectionEnd -= 1;
         }
     }
 
+    /* Deal with cases where a backspace deleting an opening bracket deletes the closing bracket straight after it as well */
     checkBackspace(codeInput, event) {
         if(event.key == "Backspace" && codeInput.textareaElement.selectionStart == codeInput.textareaElement.selectionEnd) {
-            if(this.bracketsOpenedStack.length > 0 && this.bracketsOpenedStack[this.bracketsOpenedStack.length-1][1]+1 == codeInput.textareaElement.selectionStart && codeInput.textareaElement.value[codeInput.textareaElement.selectionStart] == this.bracketsOpenedStack[this.bracketsOpenedStack.length-1][0]) {
-                // Delete closing bracket as well
+            let closingBracket = this.bracketPairs[codeInput.textareaElement.value[codeInput.textareaElement.selectionStart-1]];
+            if(closingBracket != undefined && codeInput.textareaElement.value[codeInput.textareaElement.selectionStart] == closingBracket) {
+                // Opening bracket being deleted so delete closing bracket as well
                 codeInput.textareaElement.selectionEnd = codeInput.textareaElement.selectionStart + 1;
                 codeInput.textareaElement.selectionStart -= 1;
-                this.bracketsOpenedStack.pop();
             }
         }
     }
diff --git a/plugins/autocomplete.js b/plugins/autocomplete.js
index 34700fe..24d5089 100644
--- a/plugins/autocomplete.js
+++ b/plugins/autocomplete.js
@@ -4,14 +4,14 @@
  */
 codeInput.plugins.Autocomplete = class extends codeInput.Plugin {
     /**
-     * Pass in a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd).
+     * Pass in a function to create a plugin that displays the popup that takes in (popup element, textarea, textarea.selectionEnd).
      * @param {function} updatePopupCallback  a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd).
      */
     constructor(updatePopupCallback) {
         super([]); // No observed attributes
         this.updatePopupCallback = updatePopupCallback;
     }
-    /* When a key is pressed, or scrolling occurs, update the autocomplete */
+    /* When a key is pressed, or scrolling occurs, update the popup position */
     updatePopup(codeInput, onlyScrolled) {
         let textarea = codeInput.textareaElement;
         let caretCoords = this.getCaretCoordinates(codeInput, textarea, textarea.selectionEnd, onlyScrolled);
@@ -23,20 +23,27 @@ codeInput.plugins.Autocomplete = class extends codeInput.Plugin {
             this.updatePopupCallback(popupElem, textarea, textarea.selectionEnd);
         }
     }
-    /* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
+    /* Create the popup element */
     afterElementsAdded(codeInput) {
         let popupElem = document.createElement("div");
         popupElem.classList.add("code-input_autocomplete_popup");
         codeInput.appendChild(popupElem);
 
-        let testPosElem = document.createElement("pre");
-        testPosElem.classList.add("code-input_autocomplete_testpos");
-        codeInput.appendChild(testPosElem); // Styled like first pre, but first pre found to update
-
+        let testPosPre = document.createElement("pre");
+        testPosPre.setAttribute("aria-hidden", "true"); // Hide for screen readers
+        if(codeInput.template.preElementStyled) {
+            testPosPre.classList.add("code-input_autocomplete_testpos");
+            codeInput.appendChild(testPosPre); // Styled like first pre, but first pre found to update    
+        } else {
+            let testPosCode = document.createElement("code");
+            testPosCode.classList.add("code-input_autocomplete_testpos");
+            testPosPre.appendChild(testPosCode);
+            codeInput.appendChild(testPosPre); // Styled like first pre, but first pre found to update
+        }
+        
         let textarea = codeInput.textareaElement;
-        textarea.addEventListener("keyup", this.updatePopup.bind(this, codeInput, false)); // Override this+args in bind - not just scrolling
-        textarea.addEventListener("click", this.updatePopup.bind(this, codeInput, false)); // Override this+args in bind - not just scrolling
-        textarea.addEventListener("scroll", this.updatePopup.bind(this, codeInput, true)); // Override this+args in bind - just scrolling
+        textarea.addEventListener("input", () => { this.updatePopup(codeInput, false)});
+        textarea.addEventListener("click", () => { this.updatePopup(codeInput, false)});
     }
     /**
      * Return the coordinates of the caret in a code-input
@@ -44,7 +51,7 @@ codeInput.plugins.Autocomplete = class extends codeInput.Plugin {
      * @param {HTMLElement} textarea 
      * @param {Number} charIndex 
      * @param {boolean} onlyScrolled True if no edits have been made to the text and the caret hasn't been repositioned 
-     * @returns 
+     * @returns {Object} {"top": CSS top value in pixels, "left": CSS left value in pixels}
      */
     getCaretCoordinates(codeInput, textarea, charIndex, onlyScrolled) {
         let afterSpan;
diff --git a/plugins/autodetect.js b/plugins/autodetect.js
index 9ca5f01..5b12d16 100644
--- a/plugins/autodetect.js
+++ b/plugins/autodetect.js
@@ -13,16 +13,16 @@ codeInput.plugins.Autodetect = class extends codeInput.Plugin {
         resultElement.className = ""; // CODE
         resultElement.parentElement.className = ""; // PRE
     }
-    /* Get new language class and set `lang` attribute */
+    /* Get new language class and set `language` attribute */
     afterHighlight(codeInput) {
-        let resultElement = codeInput.codeElement;
-        let langClass = resultElement.className || resultElement.parentElement.className;
+        let langClass = codeInput.codeElement.className || codeInput.preElement.className;
         let lang = langClass.match(/lang(\w|-)*/i)[0]; // Get word starting with lang...; Get outer bracket
         lang = lang.split("-")[1];
         if(lang == "undefined") {
+            codeInput.removeAttribute("language");
             codeInput.removeAttribute("lang");
         } else {
-            codeInput.setAttribute("lang", lang);
+            codeInput.setAttribute("language", lang);
         }
     }
 }
\ No newline at end of file
diff --git a/plugins/debounce-update.js b/plugins/debounce-update.js
deleted file mode 100644
index 550be7f..0000000
--- a/plugins/debounce-update.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Debounce the update and highlighting function
- * https://medium.com/@jamischarles/what-is-debouncing-2505c0648ff1
- * Files: debounce-update.js
- */
-codeInput.plugins.DebounceUpdate = class extends codeInput.Plugin {
-    /**
-     * Create a debounced update plugin to pass into a template
-     * @param {Number} delayMs Delay, in ms, to wait until updating the syntax highlighting 
-     */
-    constructor(delayMs) {
-        super([]); // No observed attributes
-        this.delayMs = delayMs;
-    }
-    /* Runs before elements are added into a `code-input`; Params: codeInput element) */
-    beforeElementsAdded(codeInput) {
-        this.update = codeInput.update.bind(codeInput); // Save previous update func
-        codeInput.update = this.updateDebounced.bind(this, codeInput);
-    }
-
-    /**
-     * Debounce the `update` function
-     */
-    updateDebounced(codeInput, text) {
-        // Editing - cancel prev. timeout
-        if(this.debounceTimeout != null) {
-            window.clearTimeout(this.debounceTimeout);
-        }
-
-        this.debounceTimeout = window.setTimeout(() => {
-            // Closure arrow function can take in variables like `text`
-            this.update(text);
-        }, this.delayMs);
-    }
-
-    // this.`update` function is original function
-
-    debounceTimeout = null; // Timeout until update
-    delayMs = 0; // Time until update
-}
\ No newline at end of file
diff --git a/plugins/debounce-update.min.js b/plugins/debounce-update.min.js
deleted file mode 100644
index 49e54c7..0000000
--- a/plugins/debounce-update.min.js
+++ /dev/null
@@ -1 +0,0 @@
-codeInput.plugins.DebounceUpdate=class extends codeInput.Plugin{constructor(a){super([]),this.delayMs=a}beforeElementsAdded(a){this.update=a.update.bind(a),a.update=this.updateDebounced.bind(this,a)}updateDebounced(a,b){null!=this.debounceTimeout&&window.clearTimeout(this.debounceTimeout),this.debounceTimeout=window.setTimeout(()=>{this.update(b)},this.delayMs)}debounceTimeout=null;delayMs=0};
\ No newline at end of file
diff --git a/plugins/go-to-line.css b/plugins/go-to-line.css
index 9a07bf0..d6a8dc6 100644
--- a/plugins/go-to-line.css
+++ b/plugins/go-to-line.css
@@ -9,21 +9,28 @@
 }
 
 .code-input_go-to_dialog {
-    position: absolute;
-    top: 0; right: 14px;
-    height: 28px;
-    padding: 6px;
-    padding-top: 8px;
-    border: solid 1px #00000044;
-    background-color: white;
-    border-radius: 6px;
-    box-shadow: 0 .2em 1em .2em rgba(0, 0, 0, 0.16);
-    animation: code-input_go-to_roll-in .2s;
-    z-index: 10;
+  position: absolute;
+  top: 0; right: 14px;
+  height: 28px;
+  padding: 6px;
+  padding-top: 8px;
+  border: solid 1px #00000044;
+  background-color: white;
+  border-radius: 6px;
+  box-shadow: 0 .2em 1em .2em rgba(0, 0, 0, 0.16);
+  z-index: 10;
 }
 
-.code-input_go-to_dialog.bye {
+.code-input_go-to_dialog:not(.code-input_go-to_hidden-dialog) {
+  animation: code-input_go-to_roll-in .2s;
+  opacity: 1;
+  pointer-events: all;
+}
+
+.code-input_go-to_dialog.code-input_go-to_hidden-dialog {
   animation: code-input_go-to_roll-out .2s;
+  opacity: 0;
+  pointer-events: none;
 }
 
 .code-input_go-to_dialog input::placeholder {
@@ -38,7 +45,7 @@
   border: 0;
 }
 
-.code-input_go-to_dialog input.error {
+.code-input_go-to_dialog input.code-input_go-to_error {
   color: #ff0000aa;
 }
 
diff --git a/plugins/go-to-line.js b/plugins/go-to-line.js
index 8cd6b74..eba0934 100644
--- a/plugins/go-to-line.js
+++ b/plugins/go-to-line.js
@@ -22,12 +22,7 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin {
         }
     }
 
-    blockSearch(dialog, event) {
-        if (event.ctrlKey && event.key == 'g') {
-            return event.preventDefault();
-        }
-    }
-
+    /* Called with a dialog box keyup event to check the validity of the line number entered and submit the dialog if Enter is pressed */
     checkPrompt(dialog, event) {
         // Line number(:column number)
         const lines = dialog.textarea.value.split('\n');
@@ -36,20 +31,24 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin {
         let columnNo = 0; // Means go to start of indented line
         let maxColumnNo = 1;
         const querySplitByColons = dialog.input.value.split(':');
-        if(querySplitByColons.length > 2) return dialog.input.classList.add('error');
-
-        if(querySplitByColons.length >= 2) {
-            columnNo = Number(querySplitByColons[1]);
-            maxColumnNo = lines[lineNo-1].length;
-        }
+        if(querySplitByColons.length > 2) return dialog.input.classList.add('code-input_go-to_error');
 
         if (event.key == 'Escape') return this.cancelPrompt(dialog, event);
 
         if (dialog.input.value) {
-            if (!/^[0-9:]*$/.test(dialog.input.value) || lineNo < 1 || columnNo < 0 || lineNo > maxLineNo || columnNo > maxColumnNo) {
-                return dialog.input.classList.add('error');
+            if (!/^[0-9:]*$/.test(dialog.input.value) || lineNo < 1 || lineNo > maxLineNo) {
+                return dialog.input.classList.add('code-input_go-to_error');
             } else {
-                dialog.input.classList.remove('error');
+                // Check if line:column
+                if(querySplitByColons.length >= 2) {
+                    columnNo = Number(querySplitByColons[1]);
+                    maxColumnNo = lines[lineNo-1].length;
+                }
+                if(columnNo < 0 || columnNo > maxColumnNo) {
+                    return dialog.input.classList.add('code-input_go-to_error');
+                } else {
+                    dialog.input.classList.remove('code-input_go-to_error');
+                }
             }
         }
 
@@ -59,21 +58,22 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin {
         }
     }
 
+    /* Called with a dialog box keyup event to close and clear the dialog box */    
     cancelPrompt(dialog, event) {
         let delay;
+        console.log("Cancel", event);
         event.preventDefault();
         dialog.textarea.focus();
 
         // Remove dialog after animation
-        dialog.classList.add('bye');
+        dialog.classList.add('code-input_go-to_hidden-dialog');
+        dialog.input.value = "";
 
         if (dialog.computedStyleMap) {
             delay = 1000 * dialog.computedStyleMap().get('animation').toString().split('s')[0];
         } else {
             delay = 1000 * document.defaultView.getComputedStyle(dialog, null).getPropertyValue('animation').split('s')[0];
         }
-
-        setTimeout(() => { dialog.codeInput.removeChild(dialog); }, .9 * delay);
     }
 
     /**
@@ -81,32 +81,41 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin {
      * @param {codeInput.CodeInput} codeInput the `` element.
     */
     showPrompt(codeInput) {
-        const textarea = codeInput.textareaElement;
-
-        const dialog = document.createElement('div');
-        const input = document.createElement('input');
-        const cancel = document.createElement('span');
-
-        dialog.appendChild(input);
-        dialog.appendChild(cancel);
-
-        dialog.className = 'code-input_go-to_dialog';
-        input.spellcheck = false;
-        input.placeholder = "Line:Column / Line no. then Enter";
-        dialog.codeInput = codeInput;
-        dialog.textarea = textarea;
-        dialog.input = input;
-
-        input.addEventListener('keydown', (event) => { this.blockSearch(dialog, event); });
-        input.addEventListener('keyup', (event) => { this.checkPrompt(dialog, event); });
-        cancel.addEventListener('click', (event) => { this.cancelPrompt(dialog, event); });
-
-        codeInput.appendChild(dialog);
-
-        input.focus();
+        if(codeInput.pluginData.goToLine == undefined || codeInput.pluginData.goToLine.dialog == undefined) {
+            const textarea = codeInput.textareaElement;
+
+            const dialog = document.createElement('div');
+            const input = document.createElement('input');
+            const cancel = document.createElement('span');
+
+            dialog.appendChild(input);
+            dialog.appendChild(cancel);
+
+            dialog.className = 'code-input_go-to_dialog';
+            input.spellcheck = false;
+            input.placeholder = "Line:Column / Line no. then Enter";
+            dialog.codeInput = codeInput;
+            dialog.textarea = textarea;
+            dialog.input = input;
+
+            input.addEventListener('keypress', (event) => {
+                /* Stop enter from submitting form */
+                if (event.key == 'Enter') event.preventDefault();
+            });
+            
+            input.addEventListener('keyup', (event) => { return this.checkPrompt(dialog, event); });
+            cancel.addEventListener('click', (event) => { this.cancelPrompt(dialog, event); });
+
+            codeInput.appendChild(dialog);
+            codeInput.pluginData.goToLine = {dialog: dialog};
+            input.focus();
+        } else {
+            codeInput.pluginData.goToLine.dialog.classList.remove("code-input_go-to_hidden-dialog");
+            codeInput.pluginData.goToLine.dialog.querySelector("input").focus();
+        }
     }
 
-    /* Set the cursor on the first non-space char of textarea's nth line; and scroll it into view */
+    /* Set the cursor on the first non-space char of textarea's nth line, or to the columnNo-numbered character in the line if it's not 0; and scroll it into view */
     goTo(textarea, lineNo, columnNo = 0) {
         let fontSize;
         let lineHeight;
@@ -146,12 +155,11 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin {
         }
     }
 
-    /* Event handlers */
+    /* Event handler for keydown event that makes Ctrl+G open go to line dialog */
     checkCtrlG(codeInput, event) {
         const textarea = codeInput.textareaElement;
         if (event.ctrlKey && event.key == 'g') {
             event.preventDefault();
-
             this.showPrompt(codeInput);
         }
     }
diff --git a/plugins/indent.js b/plugins/indent.js
index 5b515ef..113cbcb 100644
--- a/plugins/indent.js
+++ b/plugins/indent.js
@@ -5,8 +5,7 @@
  */
 codeInput.plugins.Indent = class extends codeInput.Plugin {
 
-    numSpaces;
-    bracketPairs = null; // No bracket-auto-indentation used
+    bracketPairs = {}; // No bracket-auto-indentation used when {}
     indentation = "\t";
     indentationNumChars = 1;
 
@@ -19,7 +18,6 @@ codeInput.plugins.Indent = class extends codeInput.Plugin {
     constructor(defaultSpaces=false, numSpaces=4, bracketPairs={"(": ")", "[": "]", "{": "}"}) {
         super([]); // No observed attributes
 
-        this.numSpaces = numSpaces;
         this.bracketPairs = bracketPairs;
         if(defaultSpaces) {
             this.indentation = "";
@@ -37,7 +35,7 @@ codeInput.plugins.Indent = class extends codeInput.Plugin {
         textarea.addEventListener('beforeinput', (event) => { this.checkCloseBracket(codeInput, event); });
     }
 
-    /* Event handlers */
+    /* Deal with the Tab key causing indentation, and Tab+Selection indenting / Shift+Tab+Selection unindenting lines */
     checkTab(codeInput, event) {
         if(event.key != "Tab") {
             return;
@@ -97,16 +95,17 @@ codeInput.plugins.Indent = class extends codeInput.Plugin {
             inputElement.selectionEnd = selectionEndI;
         }
 
-        codeInput.update(inputElement.value);
+        codeInput.value = inputElement.value;
     }
 
+    /* Deal with new lines retaining indentation */
     checkEnter(codeInput, event) {
         if(event.key != "Enter") {
             return;
         }
         event.preventDefault(); // Stop normal \n only
 
-        let inputElement = codeInput.querySelector("textarea");
+        let inputElement = codeInput.textareaElement;
         let lines = inputElement.value.split("\n");
         let letterI = 0;
         let currentLineI = lines.length - 1;
@@ -194,17 +193,18 @@ codeInput.plugins.Indent = class extends codeInput.Plugin {
         // Scroll down to cursor if necessary
         let paddingTop = Number(getComputedStyle(inputElement).paddingTop.replace("px", "")); 
         let lineHeight = Number(getComputedStyle(inputElement).lineHeight.replace("px", "")); 
-        let inputHeight = Number(getComputedStyle(inputElement).height.replace("px", ""));
+        let inputHeight = Number(getComputedStyle(codeInput).height.replace("px", ""));
         if(currentLineI*lineHeight + lineHeight*2 + paddingTop >= inputElement.scrollTop + inputHeight) { // Cursor too far down
-            inputElement.scrollBy(0, Number(getComputedStyle(inputElement).lineHeight.replace("px", "")))
+            codeInput.scrollBy(0, Number(getComputedStyle(inputElement).lineHeight.replace("px", "")));
         }
 
-        codeInput.update(inputElement.value);
+        codeInput.value = inputElement.value;
     }
 
+    /* Deal with one 'tab' of spaces-based-indentation being deleted by each backspace, rather than one space */
     checkBackspace(codeInput, event) {
         if(event.key != "Backspace" || this.indentationNumChars == 1) {
-            return; // Normal backspace
+            return; // Normal backspace when indentation of 1
         }
 
         let inputElement = codeInput.textareaElement;
@@ -217,6 +217,7 @@ codeInput.plugins.Indent = class extends codeInput.Plugin {
         }
     }
 
+    /* Deal with the typing of closing brackets causing a decrease in indentation */
     checkCloseBracket(codeInput, event) {
         if(codeInput.textareaElement.selectionStart != codeInput.textareaElement.selectionEnd) {
             return;
diff --git a/plugins/special-chars.css b/plugins/special-chars.css
index 20c1b1e..412c460 100644
--- a/plugins/special-chars.css
+++ b/plugins/special-chars.css
@@ -24,10 +24,6 @@
     --code-input_special-chars_F: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///38GKGAEcRgZGRlBfDAHtwxMGQDZZhP+BnB1kwAAAABJRU5ErkJggg==');
 }
 
-.code-input_special-char_container { /* pre element */
-    font-size: 20px;
-}
-
 .code-input_special-char {
     display: inline-block;
     position: relative;
diff --git a/plugins/special-chars.js b/plugins/special-chars.js
index 34e7872..00b64a4 100644
--- a/plugins/special-chars.js
+++ b/plugins/special-chars.js
@@ -7,7 +7,7 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
     specialCharRegExp;
 
     cachedColors; // ascii number > [background color, text color]
-    cachedWidths; // font > {character > character width}
+    cachedWidths; // character > character width
     canvasContext;
 
     /**
@@ -31,37 +31,31 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
         this.canvasContext = canvas.getContext("2d");
     }
 
-    /* Runs before elements are added into a `code-input`; Params: codeInput element) */
-    beforeElementsAdded(codeInput) {
-        codeInput.classList.add("code-input_special-char_container");
-    }
-
-    /* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
+    /* Initially render special characters as the highlighting algorithm may automatically highlight and remove them */
     afterElementsAdded(codeInput) {
-        // For some reason, special chars aren't synced the first time - TODO is there a cleaner way to do this?
-        setTimeout(() => { codeInput.update(codeInput.value); }, 100);
+        setTimeout(() => { codeInput.value = codeInput.value; }, 100);
     }
 
-    /* Runs after code is highlighted; Params: codeInput element) */
+    /* After highlighting, render special characters as their stylised hexadecimal equivalents */
     afterHighlight(codeInput) {      
-        let result_element = codeInput.querySelector("pre code");
+        let resultElement = codeInput.codeElement;
 
         // Reset data each highlight so can change if font size, etc. changes
         codeInput.pluginData.specialChars = {};
-        codeInput.pluginData.specialChars.textarea = codeInput.getElementsByTagName("textarea")[0];
-        codeInput.pluginData.specialChars.contrastColor = window.getComputedStyle(result_element).color;
+        codeInput.pluginData.specialChars.contrastColor = window.getComputedStyle(resultElement).color;
 
-        this.recursivelyReplaceText(codeInput, result_element);
+        this.recursivelyReplaceText(codeInput, resultElement);
 
-        this.lastFont = window.getComputedStyle(codeInput.pluginData.specialChars.textarea).font;
+        this.lastFont = window.getComputedStyle(codeInput.textareaElement).font;
     }
 
+    /* Search for special characters in an element and replace them with their stylised hexadecimal equivalents */
     recursivelyReplaceText(codeInput, element) {
         for(let i = 0; i < element.childNodes.length; i++) {
 
             let nextNode = element.childNodes[i];
-            if(nextNode.nodeName == "#text" && nextNode.nodeValue != "") {
-                // Replace in here
+            if(nextNode.nodeType == 3) {
+                // Text node - Replace in here
                 let oldValue = nextNode.nodeValue;
 
                 this.specialCharRegExp.lastIndex = 0;
@@ -76,7 +70,7 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
                     }
 
                     if(nextNode.textContent != "") {
-                        let replacementElement = this.specialCharReplacer(codeInput, nextNode.textContent);
+                        let replacementElement = this.getStylisedSpecialChar(codeInput, nextNode.textContent);
                         nextNode.parentNode.insertBefore(replacementElement, nextNode);
                         nextNode.textContent = "";
                     }
@@ -90,29 +84,30 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
         }
     }
 
-    specialCharReplacer(codeInput, match_char) {
-        let hex_code = match_char.codePointAt(0);
+    /* Get the stylised hexadecimal representation HTML element for a given special character */
+    getStylisedSpecialChar(codeInput, matchChar) {
+        let hexCode = matchChar.codePointAt(0);
 
         let colors;
-        if(this.colorInSpecialChars) colors = this.getCharacterColor(hex_code);
+        if(this.colorInSpecialChars) colors = this.getCharacterColors(hexCode);
 
-        hex_code = hex_code.toString(16);
-        hex_code = ("0000" + hex_code).substring(hex_code.length); // So 2 chars with leading 0
-        hex_code = hex_code.toUpperCase();
+        hexCode = hexCode.toString(16);
+        hexCode = ("0000" + hexCode).substring(hexCode.length); // So 2 chars with leading 0
+        hexCode = hexCode.toUpperCase();
 
-        let char_width = this.getCharacterWidth(codeInput, match_char);
+        let charWidth = this.getCharacterWidthEm(codeInput, matchChar);
 
         // Create element with hex code
         let result = document.createElement("span");
         result.classList.add("code-input_special-char");
-        result.style.setProperty("--hex-0",  "var(--code-input_special-chars_" + hex_code[0] + ")");
-        result.style.setProperty("--hex-1",  "var(--code-input_special-chars_" + hex_code[1] + ")");
-        result.style.setProperty("--hex-2",  "var(--code-input_special-chars_" + hex_code[2] + ")");
-        result.style.setProperty("--hex-3",  "var(--code-input_special-chars_" + hex_code[3] + ")");
+        result.style.setProperty("--hex-0",  "var(--code-input_special-chars_" + hexCode[0] + ")");
+        result.style.setProperty("--hex-1",  "var(--code-input_special-chars_" + hexCode[1] + ")");
+        result.style.setProperty("--hex-2",  "var(--code-input_special-chars_" + hexCode[2] + ")");
+        result.style.setProperty("--hex-3",  "var(--code-input_special-chars_" + hexCode[3] + ")");
         
         // Handle zero-width chars
-        if(char_width == 0) result.classList.add("code-input_special-char_zero-width");
-        else result.style.width = char_width + "px";
+        if(charWidth == 0) result.classList.add("code-input_special-char_zero-width");
+        else result.style.width = charWidth + "em";
 
         if(this.colorInSpecialChars) {
             result.style.backgroundColor = "#" + colors[0];
@@ -123,64 +118,71 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
         return result;
     }
     
-    getCharacterColor(ascii_code) {
-        // Choose colors based on character code - lazy load and return [background color, text color]
-        let background_color;
-        let text_color;
-        if(!(ascii_code in this.cachedColors)) {
+    /* Get the colors a stylised representation of a given character must be shown in; lazy load and return [background color, text color] */
+    getCharacterColors(asciiCode) {
+        let backgroundColor;
+        let textColor;
+        if(!(asciiCode in this.cachedColors)) {
             // Get background color - arbitrary bit manipulation to get a good range of colours
-            background_color = ascii_code^(ascii_code << 3)^(ascii_code << 7)^(ascii_code << 14)^(ascii_code << 16); // Arbitrary
-            background_color = background_color^0x1fc627; // Arbitrary
-            background_color = background_color.toString(16);
-            background_color = ("000000" + background_color).substring(background_color.length); // So 6 chars with leading 0
+            backgroundColor = asciiCode^(asciiCode << 3)^(asciiCode << 7)^(asciiCode << 14)^(asciiCode << 16); // Arbitrary
+            backgroundColor = backgroundColor^0x1fc627; // Arbitrary
+            backgroundColor = backgroundColor.toString(16);
+            backgroundColor = ("000000" + backgroundColor).substring(backgroundColor.length); // So 6 chars with leading 0
 
             // Get most suitable text color - white or black depending on background brightness
-            let color_brightness = 0;
-            let luminance_coefficients = [0.299, 0.587, 0.114];
+            let colorBrightness = 0;
+            const luminanceCoefficients = [0.299, 0.587, 0.114];
             for(let i = 0; i < 6; i += 2) {
-                color_brightness += parseInt(background_color.substring(i, i+2), 16) * luminance_coefficients[i/2];
+                colorBrightness += parseInt(backgroundColor.substring(i, i+2), 16) * luminanceCoefficients[i/2];
             }
             // Calculate darkness
-            text_color = color_brightness < 128 ? "white" : "black";
+            textColor = colorBrightness < 128 ? "white" : "black";
 
-            this.cachedColors[ascii_code] = [background_color, text_color];
-            return [background_color, text_color];
+            this.cachedColors[asciiCode] = [backgroundColor, textColor];
+            return [backgroundColor, textColor];
         } else {
-            return this.cachedColors[ascii_code];
+            return this.cachedColors[asciiCode];
         }
     }
 
-    getCharacterWidth(codeInput, char) {
+    /* Get the width of a character in em (relative to font size), for use in creation of the stylised hexadecimal representation with the same width */
+    getCharacterWidthEm(codeInput, char) {
         // Force zero-width characters
         if(new RegExp("\u00AD|\u02de|[\u0300-\u036F]|[\u0483-\u0489]|\u200b").test(char) ) { return 0 }
         // Non-renderable ASCII characters should all be rendered at same size
         if(char != "\u0096" && new RegExp("[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]", "g").test(char)) {
-            let fallbackWidth = this.getCharacterWidth("\u0096");
+            let fallbackWidth = this.getCharacterWidthEm(codeInput, "\u0096");
             return fallbackWidth;
         }
 
-        let font = window.getComputedStyle(codeInput.pluginData.specialChars.textarea).font;
+        let font = getComputedStyle(codeInput.textareaElement).fontFamily + " " + getComputedStyle(codeInput.textareaElement).fontStretch + " " + getComputedStyle(codeInput.textareaElement).fontStyle + " " + getComputedStyle(codeInput.textareaElement).fontVariant + " " + getComputedStyle(codeInput.textareaElement).fontWeight + " " + getComputedStyle(codeInput.textareaElement).lineHeight; // Font without size
 
-        // Lazy-load - TODO: Get a cleaner way of doing this
+        // Lazy-load width of each character
         if(this.cachedWidths[font] == undefined) {
-            this.cachedWidths[font] = {}; // Create new cached widths for this font
+            this.cachedWidths[font] = {};
         }
         if(this.cachedWidths[font][char] != undefined) { // Use cached width
             return this.cachedWidths[font][char];
         }
 
-        // Ensure font the same
-        this.canvasContext.font = font;
+        // Ensure font the same - 20px font size is where this algorithm works
+        this.canvasContext.font = getComputedStyle(codeInput.textareaElement).font.replace(getComputedStyle(codeInput.textareaElement).fontSize, "20px");
 
         // Try to get width from canvas
-        let width = this.canvasContext.measureText(char).width;
-        if(width > Number(font.split("px")[0])) {
+        let width = this.canvasContext.measureText(char).width/20; // From px to em (=proportion of font-size)
+        if(width > 1) {
             width /= 2; // Fix double-width-in-canvas Firefox bug
         } else if(width == 0 && char != "\u0096") {
-            let fallbackWidth = this.getCharacterWidth("\u0096");
+            let fallbackWidth = this.getCharacterWidthEm(codeInput, "\u0096");
             return fallbackWidth; // In Firefox some control chars don't render, but all control chars are the same width
         }
 
+        // Firefox will never make smaller than size at 20px
+        if(navigator.userAgent.includes("Mozilla") && !navigator.userAgent.includes("Chrome") && !navigator.userAgent.includes("Safari")) {
+            let fontSize = Number(getComputedStyle(codeInput.textareaElement).fontSize.substring(0, getComputedStyle(codeInput.textareaElement).fontSize.length-2)); // Remove 20, make px
+            if(fontSize < 20) width *= 20 / fontSize;
+        }
+
         this.cachedWidths[font][char] = width;
 
         return width;
diff --git a/plugins/test.js b/plugins/test.js
index ae7fa0b..0e1fbb6 100644
--- a/plugins/test.js
+++ b/plugins/test.js
@@ -10,9 +10,8 @@
  */
 codeInput.plugins.Test = class extends codeInput.Plugin {
     constructor() {
-        super(["testattr", "test-*"]); 
+        super(["testattr"]); 
         // Array of observed attributes as parameter
-        // Wildcard "*" matches any text
     }
     /* Runs before code is highlighted; Params: codeInput element) */
     beforeHighlight(codeInput) {
diff --git a/tests/hljs.html b/tests/hljs.html
new file mode 100644
index 0000000..a832801
--- /dev/null
+++ b/tests/hljs.html
@@ -0,0 +1,51 @@
+
+
+
+    
+    
+    code-input Tester
+
+    
+    
+    
+    
+    
+    
+    
+    
+
+    
+    
+    
+
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+
+    
+
+
+    

code-input Tester (highlight.js)

+

Test for Prism.js

+

This page carries out automated tests for the code-input library to check that both the core components and the plugins work in some ways. It doesn't fully cover every scenario so you should test any code you change by hand, but it's good for quickly checking a wide range of functionality works.

+ +
Test Results (Click to Open)
+
+ console.log("Hello, World!"); +// A second line +// A third line with <html> tags + +
+ + + + \ No newline at end of file diff --git a/tests/prism.html b/tests/prism.html new file mode 100644 index 0000000..71c2628 --- /dev/null +++ b/tests/prism.html @@ -0,0 +1,45 @@ + + + + + + code-input Tester + + + + + + + + + + + + + + + + + + + + + + + +

code-input Tester (Prism.js)

+

Test for highlight.js

+

This page carries out automated tests for the code-input library to check that both the core components and the plugins work in some ways. It doesn't fully cover every scenario so you should test any code you change by hand, but it's good for quickly checking a wide range of functionality works.

+ +
Test Results (Click to Open)
+
+ console.log("Hello, World!"); +// A second line +// A third line with <html> tags + +
+ + + \ No newline at end of file diff --git a/tests/tester.js b/tests/tester.js new file mode 100644 index 0000000..e0ea13f --- /dev/null +++ b/tests/tester.js @@ -0,0 +1,398 @@ +/* Main testing code */ + +function beginTest(isHLJS) { + let codeInputElem = document.querySelector("code-input"); + if(isHLJS) { + codeInput.registerTemplate("code-editor", codeInput.templates.hljs(hljs, [ + new codeInput.plugins.AutoCloseBrackets(), + new codeInput.plugins.Autocomplete(function(popupElem, textarea, selectionEnd) { + if(textarea.value.substring(selectionEnd-5, selectionEnd) == "popup") { + // Show popup + popupElem.style.display = "block"; + popupElem.innerHTML = "Here's your popup!"; + } else { + popupElem.style.display = "none"; + } + }), + new codeInput.plugins.Autodetect(), + new codeInput.plugins.GoToLine(), + new codeInput.plugins.Indent(true, 2), + new codeInput.plugins.SpecialChars(true), + ])); + } else { + codeInput.registerTemplate("code-editor", codeInput.templates.prism(Prism, [ + new codeInput.plugins.AutoCloseBrackets(), + new codeInput.plugins.Autocomplete(function(popupElem, textarea, selectionEnd) { + if(textarea.value.substring(selectionEnd-5, selectionEnd) == "popup") { + // Show popup + popupElem.style.display = "block"; + popupElem.innerHTML = "Here's your popup!"; + } else { + popupElem.style.display = "none"; + } + }), + new codeInput.plugins.GoToLine(), + new codeInput.plugins.Indent(true, 2), + new codeInput.plugins.SpecialChars(true), + ])); + } + startLoad(codeInputElem, isHLJS); +} + +function testData(group, test, data) { + let resultElem = document.getElementById("test-results"); + let groupElem = resultElem.querySelector("#test-"+group); + if(groupElem == undefined) { + groupElem = document.createElement("span"); + groupElem.innerHTML = `Group ${group}:\n` + groupElem.id = "test-" + group; + resultElem.append(groupElem); + } + groupElem.innerHTML += `\t${test}: ${data}\n`; +} + +function testAssertion(group, test, passed, messageIfFailed) { + let resultElem = document.getElementById("test-results"); + let groupElem = resultElem.querySelector("#test-"+group); + if(groupElem == undefined) { + groupElem = document.createElement("span"); + groupElem.innerHTML = `Group ${group}:\n` + groupElem.id = "test-" + group; + resultElem.append(groupElem); + } + groupElem.innerHTML += `\t${test}: ${passed ? 'passed' : 'failed ('+messageIfFailed+')' }\n`; +} + +function assertEqual(group, test, givenOutput, correctOutput) { + let equal = givenOutput == correctOutput; + testAssertion(group, test, equal, "see console output"); + if(!equal) { + console.error(group, test, givenOutput, "should be", correctOutput) + } +} + +function testAddingText(group, textarea, action, correctOutput, correctLengthToSelectionStart, correctLengthToSelectionEnd) { + let origSelectionStart = textarea.selectionStart; + let origValueBefore = textarea.value.substring(0, textarea.selectionStart); + let origValueAfter = textarea.value.substring(textarea.selectionEnd); + action(textarea); + + let correctOutputValue = origValueBefore+correctOutput+origValueAfter; + assertEqual(group, "Text Output", textarea.value, correctOutputValue) + assertEqual(group, "Code-Input Value JS Property Output", textarea.parentElement.value, correctOutputValue) + assertEqual(group, "Selection Start", textarea.selectionStart, origSelectionStart+correctLengthToSelectionStart) + assertEqual(group, "Selection End", textarea.selectionEnd, origSelectionStart+correctLengthToSelectionEnd) +} + +function startLoad(codeInputElem, isHLJS) { + let textarea; + let timeToLoad = 0; + let interval = window.setInterval(() => { + textarea = codeInputElem.querySelector("textarea"); + if(textarea != null) window.clearInterval(interval); + timeToLoad += 10; + testData("TimeTaken", "Textarea Appears", timeToLoad+"ms (nearest 10)"); + startTests(textarea, isHLJS); + }, 10); +} + +function addText(textarea, text, enterEvents=false) { + for(let i = 0; i < text.length; i++) { + if(enterEvents && text[i] == "\n") { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); + textarea.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" })); + } else { + let beforeInputEvt = new InputEvent("beforeinput", { "cancelable": true, "data": text[i] }); + textarea.dispatchEvent(beforeInputEvt); + if(!beforeInputEvt.defaultPrevented) { + textarea.dispatchEvent(new InputEvent("input", { "data": text[i] })); + } + } + } +} +function backspace(textarea) { + let keydownEvt = new KeyboardEvent("keydown", { "cancelable": true, "key": "Backspace" }); + textarea.dispatchEvent(keydownEvt); + let keyupEvt = new KeyboardEvent("keyup", { "cancelable": true, "key": "Backspace" }); + textarea.dispatchEvent(keyupEvt); + if(!keydownEvt.defaultPrevented) { + if(textarea.selectionEnd == textarea.selectionStart) { + textarea.selectionEnd = textarea.selectionStart; + textarea.selectionStart--; + } + document.execCommand("delete", false, null); + } +} + +function move(textarea, numMovesRight) { + textarea.selectionStart += numMovesRight; + textarea.selectionEnd = textarea.selectionStart; +} + +function waitAsync(milliseconds) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, milliseconds); + }); +} + +async function startTests(textarea, isHLJS) { + // Make input events trusted - thanks for this SO answer: https://stackoverflow.com/a/49519772/21785620 + textarea.addEventListener('input', function(e){ + if(!e.isTrusted){ + //Manually triggered + document.execCommand("insertText", false, e.data); + } + }, false); + + textarea.focus(); + + codeInputElement = textarea.parentElement; + + /*--- Tests for core functionality ---*/ + // Textarea's initial value should be correct. + assertEqual("Core", "Initial Textarea Value", textarea.value, `console.log("Hello, World!"); +// A second line +// A third line with tags`); + // Code element's displayed value, ignoring appearance with HTML tags, should be the initial value but HTML-escaped + let renderedValue = codeInputElement.codeElement.innerHTML.replace(/<[^>]+>/g, ""); + assertEqual("Core", "Initial Rendered Value", renderedValue, `console.log("Hello, World!"); +// A second line +// A third line with <html> tags`); + + + // Update code-input value with JavaScript, new value should be correct. + codeInputElement.value += ` +console.log("I've got another line!", 2 < 3, "should be true.");`; + + await waitAsync(50); // Wait for rendered value to update + + // Textarea's value once updated with JavaScript should be correct. + assertEqual("Core", "JS-updated Textarea Value", textarea.value, `console.log("Hello, World!"); +// A second line +// A third line with tags +console.log("I've got another line!", 2 < 3, "should be true.");`); + // Code element's displayed value, ignoring appearance with HTML tags, should be the initial value but HTML-escaped + renderedValue = codeInputElement.codeElement.innerHTML.replace(/<[^>]+>/g, ""); + assertEqual("Core", "JS-updated Rendered Value", renderedValue, `console.log("Hello, World!"); +// A second line +// A third line with <html> tags +console.log("I've got another line!", 2 < 3, "should be true.");`); + + // Changing language should be correct + if(!isHLJS) { + // Highlight.js has autodetect plugin that should make this fail, so don't run this test. + testAssertion("Core", "Language attribute Initial value", + codeInputElement.codeElement.classList.contains("language-javascript") + && !codeInputElement.codeElement.classList.contains("language-html"), + `Language set to JavaScript but code element's class name is ${codeInputElement.codeElement.className}.`); + + codeInputElement.setAttribute("language", "HTML"); + + await waitAsync(50); // Wait for attribute change to be handled + + // Highlight.js has autodetect plugin that should make this fail, so don't run this test. + testAssertion("Core", "Language attribute Changed value 1", + codeInputElement.codeElement.classList.contains("language-html") + && !codeInputElement.codeElement.classList.contains("language-javascript"), + `Language set to HTML but code element's class name is ${codeInputElement.codeElement.className}.`); + + codeInputElement.setAttribute("language", "JavaScript"); + + await waitAsync(50); // Wait for attribute change to be handled + + // Highlight.js has autodetect plugin that should make this fail, so don't run this test. + testAssertion("Core", "Language attribute Changed value 2", + codeInputElement.codeElement.classList.contains("language-javascript") + && !codeInputElement.codeElement.classList.contains("language-html"), + `Language set to JavaScript but code element's class name is ${codeInputElement.codeElement.className}.`); + } + + let formElement = codeInputElement.parentElement; + formElement.reset(); + + await waitAsync(50); // Wait for rendered value to update + + assertEqual("Core", "Form Reset resets Code-Input Value", codeInputElement.value, `console.log("Hello, World!"); +// A second line +// A third line with tags`); + assertEqual("Core", "Form Reset resets Textarea Value", textarea.value, `console.log("Hello, World!"); +// A second line +// A third line with tags`); + renderedValue = codeInputElement.codeElement.innerHTML.replace(/<[^>]+>/g, ""); + assertEqual("Core", "Form Reset resets Rendered Value", renderedValue, `console.log("Hello, World!"); +// A second line +// A third line with <html> tags`); + + /*--- Tests for plugins ---*/ + // AutoCloseBrackets + testAddingText("AutoCloseBrackets", textarea, function(textarea) { + addText(textarea, `\nconsole.log("A test message`); + move(textarea, 2); + addText(textarea, `;\nconsole.log("Another test message");\n{[{[]}(([[`); + backspace(textarea); + backspace(textarea); + backspace(textarea); + addText(textarea, `)`); + }, '\nconsole.log("A test message");\nconsole.log("Another test message");\n{[{[]}()]}', 77, 77); + + // Autocomplete + addText(textarea, "popup"); + + await waitAsync(50); // Wait for popup to be rendered + + testAssertion("Autocomplete", "Popup Shows", confirm("Does the autocomplete popup display correctly? (OK=Yes)"), "user-judged"); + backspace(textarea); + + await waitAsync(50); // Wait for popup disappearance to be rendered + + testAssertion("Autocomplete", "Popup Disappears", confirm("Has the popup disappeared? (OK=Yes)"), "user-judged"); + backspace(textarea); + backspace(textarea); + backspace(textarea); + backspace(textarea); + + // Autodetect - these tests have been made so the programming language is very obvious + // - the efficacy of autodetection is highlight.js' responsibility. + if(isHLJS) { + // Check detects XML - Replace all code with XML + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + backspace(textarea); + addText(textarea, 'console.log("Hello, World!");\nfunction sayHello(name) {\n console.log("Hello, " + name + "!");\n}\nsayHello("code-input");'); + await waitAsync(50); // Wait for highlighting so language attribute updates + assertEqual("Autodetect", "Detects JavaScript", codeInputElement.getAttribute("language"), "javascript"); + + // Check detects Python - Replace all code with Python + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + backspace(textarea); + addText(textarea, '#!/usr/bin/python\nprint("Hello, World!")\nfor i in range(5):\n print(i)'); + await waitAsync(50); // Wait for highlighting so language attribute updates + assertEqual("Autodetect", "Detects Python", codeInputElement.getAttribute("language"), "python"); + + // Check detects CSS - Replace all code with CSS + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + backspace(textarea); + addText(textarea, "body, html {\n height: 100%;\n background-color: blue;\n color: red;\n}"); + await waitAsync(50); // Wait for highlighting so language attribute updates + assertEqual("Autodetect", "Detects CSS", codeInputElement.getAttribute("language"), "css"); + } + + // GoToLine + // Replace all code + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + backspace(textarea); + addText(textarea, "// 7 times table\nlet i = 1;\nwhile(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n// That's my code.\n// This is another comment\n// Another\n// Line"); + + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); + let dialog = codeInputElement.querySelector(".code-input_go-to_dialog input"); + dialog.value = "1"; + dialog.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); + dialog.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" })); + assertEqual("GoToLine", "Line Only", textarea.selectionStart, 0); + + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); + dialog = codeInputElement.querySelector(".code-input_go-to_dialog input"); + dialog.value = "3:18"; + dialog.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); + dialog.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" })); + assertEqual("GoToLine", "Line and Column", textarea.selectionStart, 45); + + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); + dialog = codeInputElement.querySelector(".code-input_go-to_dialog input"); + dialog.value = "10"; + dialog.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); + dialog.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" })); + assertEqual("GoToLine", "Rejects Out-of-range Line", dialog.classList.contains("code-input_go-to_error"), true); + + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); + dialog = codeInputElement.querySelector(".code-input_go-to_dialog input"); + dialog.value = "2:12"; + dialog.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); + dialog.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" })); + assertEqual("GoToLine", "Rejects Out-of-range Column", dialog.classList.contains("code-input_go-to_error"), true); + + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); + dialog = codeInputElement.querySelector(".code-input_go-to_dialog input"); + dialog.value = "sausages"; + dialog.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); + dialog.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" })); + assertEqual("GoToLine", "Rejects Invalid Input", dialog.classList.contains("code-input_go-to_error"), true); + assertEqual("GoToLine", "Stays open when Rejects Input", dialog.parentElement.classList.contains("code-input_go-to_hidden-dialog"), false); + + dialog.dispatchEvent(new KeyboardEvent("keydown", { "key": "Escape" })); + dialog.dispatchEvent(new KeyboardEvent("keyup", { "key": "Escape" })); + assertEqual("GoToLine", "Exits when Esc pressed", dialog.parentElement.classList.contains("code-input_go-to_hidden-dialog"), true); + + // Indent + textarea.selectionStart = textarea.selectionEnd = textarea.value.length; + addText(textarea, "\nfor(let i = 0; i < 100; i++) {\n for(let j = i; j < 100; j++) {\n // Here's some code\n console.log(i,j);\n }\n}\n{\n // This is indented\n}") + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + textarea.dispatchEvent(new KeyboardEvent("keydown", { "key": "Tab", "shiftKey": false })); + textarea.dispatchEvent(new KeyboardEvent("keyup", { "key": "Tab", "shiftKey": false })); + assertEqual("Indent", "Indents Lines", textarea.value, " // 7 times table\n let i = 1;\n while(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n // That's my code.\n // This is another comment\n // Another\n // Line\n for(let i = 0; i < 100; i++) {\n for(let j = i; j < 100; j++) {\n // Here's some code\n console.log(i,j);\n }\n }\n {\n // This is indented\n }"); + textarea.dispatchEvent(new KeyboardEvent("keydown", { "key": "Tab", "shiftKey": true })); + textarea.dispatchEvent(new KeyboardEvent("keyup", { "key": "Tab", "shiftKey": true })); + assertEqual("Indent", "Unindents Lines", textarea.value, "// 7 times table\nlet i = 1;\nwhile(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n// That's my code.\n// This is another comment\n// Another\n// Line\nfor(let i = 0; i < 100; i++) {\n for(let j = i; j < 100; j++) {\n // Here's some code\n console.log(i,j);\n }\n}\n{\n // This is indented\n}"); + textarea.dispatchEvent(new KeyboardEvent("keydown", { "key": "Tab", "shiftKey": true })); + textarea.dispatchEvent(new KeyboardEvent("keyup", { "key": "Tab", "shiftKey": true })); + assertEqual("Indent", "Unindents Lines where some are already fully unindented", textarea.value, "// 7 times table\nlet i = 1;\nwhile(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n// That's my code.\n// This is another comment\n// Another\n// Line\nfor(let i = 0; i < 100; i++) {\nfor(let j = i; j < 100; j++) {\n // Here's some code\n console.log(i,j);\n}\n}\n{\n// This is indented\n}"); + + textarea.selectionStart = 255; + textarea.selectionEnd = 274; + textarea.dispatchEvent(new KeyboardEvent("keydown", { "key": "Tab", "shiftKey": false })); + textarea.dispatchEvent(new KeyboardEvent("keyup", { "key": "Tab", "shiftKey": false })); + assertEqual("Indent", "Indents Lines by Selection", textarea.value, "// 7 times table\nlet i = 1;\nwhile(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n// That's my code.\n// This is another comment\n// Another\n// Line\nfor(let i = 0; i < 100; i++) {\nfor(let j = i; j < 100; j++) {\n // Here's some code\n console.log(i,j);\n}\n}\n{\n // This is indented\n}"); + + textarea.selectionStart = 265; + textarea.selectionEnd = 265; + textarea.dispatchEvent(new KeyboardEvent("keydown", { "key": "Tab", "shiftKey": true })); + textarea.dispatchEvent(new KeyboardEvent("keyup", { "key": "Tab", "shiftKey": true })); + assertEqual("Indent", "Unindents Lines by Selection", textarea.value, "// 7 times table\nlet i = 1;\nwhile(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n// That's my code.\n// This is another comment\n// Another\n// Line\nfor(let i = 0; i < 100; i++) {\nfor(let j = i; j < 100; j++) {\n // Here's some code\n console.log(i,j);\n}\n}\n{\n// This is indented\n}"); + + // Indent+AutoCloseBrackets + // Clear all code + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + backspace(textarea); + + testAddingText("Indent-AutoCloseBrackets", textarea, function(textarea) { + addText(textarea, `function printTriples(max) {\nfor(let i = 0; i < max-2; i++) {\nfor(let j = 0; j < max-1; j++) {\nfor(let k = 0; k < max; k++) {\nconsole.log(i,j,k);\n}\n//Hmmm...`, true) + }, 'function printTriples(max) {\n for(let i = 0; i < max-2; i++) {\n for(let j = 0; j < max-1; j++) {\n for(let k = 0; k < max; k++) {\n console.log(i,j,k);\n }\n //Hmmm...\n }\n }\n }\n}', 189, 189); + + // Special Chars + // Clear all code + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + backspace(textarea); + + addText(textarea, '"Some special characters: \u0096,\u0001\u0003,\u0002..."'); + textarea.selectionStart = textarea.value.length-4; + textarea.selectionEnd = textarea.value.length; + + await waitAsync(50); // Wait for special characters to be rendered + + testAssertion("SpecialChars", "Displays Correctly", confirm("Do the special characters read (0096),(0001)(0003),(0002) and align with the ellipsis? (OK=Yes)"), "user-judged"); + + // Large amounts of code + // Clear all code + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + backspace(textarea); + fetch(new Request("https://cdn.jsdelivr.net/gh/webcoder49/code-input@2.1/code-input.js")) + .then((response) => response.text()) + .then((code) => { + textarea.value = "// code-input v2.1: A large code file (not the latest version!)\n// Editing this here should give little latency.\n\n"+code; + + textarea.selectionStart = 112; + textarea.selectionEnd = 112; + addText(textarea, "\n", true); + + document.getElementById("collapse-results").setAttribute("open", true); + }); +} \ No newline at end of file