From dd1fc5421774b37d9e756c2e4f974596fca461a9 Mon Sep 17 00:00:00 2001 From: WebCoder49 Date: Fri, 15 Dec 2023 08:36:58 +0000 Subject: [PATCH 01/11] Started speeding up updates using requestAnimationFrame; Ctrl+F fix still iffy --- code-input.css | 28 ++++++---- code-input.js | 105 +++++++++++++++---------------------- plugins/debounce-update.js | 2 +- plugins/indent.js | 6 +-- plugins/special-chars.js | 2 +- 5 files changed, 66 insertions(+), 77 deletions(-) diff --git a/code-input.css b/code-input.css index ef1b23c..fed9ec5 100644 --- a/code-input.css +++ b/code-input.css @@ -9,8 +9,7 @@ code-input { top: 0; left: 0; display: block; - /* Only scroll inside elems */ - overflow: hidden; + overflow: auto; /* Normal inline styles */ margin: 8px; @@ -24,22 +23,33 @@ code-input { caret-color: darkgrey; white-space: pre; padding: 0!important; /* Use --padding */ + + display: grid; + grid-template-columns: auto; + grid-template-rows: auto; } -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; + width: calc(100% - var(--padding, 16px) * 2); + min-height: calc(100% - var(--padding, 16px) * 2); + + overflow: hidden; + resize: none; + + grid-row: 1 2; + grid-column: 1 2; } code-input:not(.code-input_pre-element-styled) pre, code-input.code-input_pre-element-styled pre code { @@ -96,8 +106,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; diff --git a/code-input.js b/code-input.js index 987f54e..31efa74 100644 --- a/code-input.js +++ b/code-input.js @@ -513,30 +513,49 @@ 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 + needsDisableDuplicateSearching = false; // Just highlighted + + /** + * Highlight the code ASAP */ - 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; + scheduleHighlight() { + this.needsHighlight = true; + } + + /** + * Call an animation frame + */ + animateFrame() { + // Sync size + this.textareaElement.style.height = getComputedStyle(this.preElement).height; + this.textareaElement.style.width = getComputedStyle(this.preElement).width; + + // Sync content + if(this.needsHighlight) { + console.log("Update"); + this.update(); + this.needsHighlight = false; + this.needsDisableDuplicateSearching = true; + } + if(this.needsDisableDuplicateSearching && this.codeElement.querySelector("*") != null) { + // Has been highlighted + this.resultElementDisableSearching(); + this.needsDisableDuplicateSearching = false; } - this.ignoreValueUpdate = true; - this.value = value; - this.ignoreValueUpdate = false; - if (this.textareaElement.value != value) this.textareaElement.value = value; + 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 +568,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; } /** @@ -694,8 +682,7 @@ var codeInput = { } }); - 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; @@ -720,16 +707,10 @@ var codeInput = { 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(); } /** @@ -822,7 +803,7 @@ var codeInput = { 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; @@ -854,7 +835,7 @@ var codeInput = { if (mainTextarea.placeholder == oldValue) mainTextarea.placeholder = newValue; - this.update(this.value); + this.needsHighlight = true; break; default: @@ -954,7 +935,7 @@ var codeInput = { val = ""; } this._value = val; - this.update(val); + this.needsHighlight = true; return val; } @@ -1032,7 +1013,7 @@ var codeInput = { * Update value on form reset */ formResetCallback() { - this.update(this.initialValue); + this.value = this.initialValue; }; }, diff --git a/plugins/debounce-update.js b/plugins/debounce-update.js index 550be7f..7d8e1b3 100644 --- a/plugins/debounce-update.js +++ b/plugins/debounce-update.js @@ -29,7 +29,7 @@ codeInput.plugins.DebounceUpdate = class extends codeInput.Plugin { this.debounceTimeout = window.setTimeout(() => { // Closure arrow function can take in variables like `text` - this.update(text); + codeInput.value = text; }, this.delayMs); } diff --git a/plugins/indent.js b/plugins/indent.js index 5b515ef..8353683 100644 --- a/plugins/indent.js +++ b/plugins/indent.js @@ -97,7 +97,7 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { inputElement.selectionEnd = selectionEndI; } - codeInput.update(inputElement.value); + codeInput.value = inputElement.value; } checkEnter(codeInput, event) { @@ -196,10 +196,10 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { let lineHeight = Number(getComputedStyle(inputElement).lineHeight.replace("px", "")); let inputHeight = Number(getComputedStyle(inputElement).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; } checkBackspace(codeInput, event) { diff --git a/plugins/special-chars.js b/plugins/special-chars.js index 34e7872..dc14256 100644 --- a/plugins/special-chars.js +++ b/plugins/special-chars.js @@ -39,7 +39,7 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin { /* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */ 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) */ From 0c3af7c4ca4f67431f92b308139d709a39b9b7c7 Mon Sep 17 00:00:00 2001 From: WebCoder49 Date: Fri, 15 Dec 2023 09:03:43 +0000 Subject: [PATCH 02/11] Some compatibility changes in plugins; disabled messy Ctrl+F fix for now --- code-input.css | 11 +++++------ code-input.js | 14 +++++++------- plugins/indent.js | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/code-input.css b/code-input.css index fed9ec5..0344708 100644 --- a/code-input.css +++ b/code-input.css @@ -9,7 +9,8 @@ code-input { top: 0; left: 0; display: block; - overflow: auto; + overflow-y: auto; + overflow-x: auto; /* Normal inline styles */ margin: 8px; @@ -42,14 +43,12 @@ code-input textarea, code-input:not(.code-input_pre-element-styled) pre code, co margin: 0px!important; padding: var(--padding, 16px)!important; border: 0; - width: calc(100% - var(--padding, 16px) * 2); + min-width: calc(100% - var(--padding, 16px) * 2); min-height: calc(100% - var(--padding, 16px) * 2); - overflow: hidden; resize: none; - - grid-row: 1 2; - grid-column: 1 2; + grid-row: 1; + grid-column: 1; } code-input:not(.code-input_pre-element-styled) pre, code-input.code-input_pre-element-styled pre code { diff --git a/code-input.js b/code-input.js index 31efa74..770dc4a 100644 --- a/code-input.js +++ b/code-input.js @@ -514,7 +514,7 @@ var codeInput = { * to syntax-highlight it. */ needsHighlight = false; // Just inputted - needsDisableDuplicateSearching = false; // Just highlighted + // needsDisableDuplicateSearching = false; // Just highlighted /** * Highlight the code ASAP @@ -536,13 +536,13 @@ var codeInput = { console.log("Update"); this.update(); this.needsHighlight = false; - this.needsDisableDuplicateSearching = true; - } - if(this.needsDisableDuplicateSearching && this.codeElement.querySelector("*") != null) { - // Has been highlighted - this.resultElementDisableSearching(); - this.needsDisableDuplicateSearching = false; + // this.needsDisableDuplicateSearching = true; } + // if(this.needsDisableDuplicateSearching && this.codeElement.querySelector("*") != null) { + // // Has been highlighted + // this.resultElementDisableSearching(); + // this.needsDisableDuplicateSearching = false; + // } window.requestAnimationFrame(this.animateFrame.bind(this)); } diff --git a/plugins/indent.js b/plugins/indent.js index 8353683..36aa418 100644 --- a/plugins/indent.js +++ b/plugins/indent.js @@ -194,7 +194,7 @@ 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 codeInput.scrollBy(0, Number(getComputedStyle(inputElement).lineHeight.replace("px", ""))); } From e63e6d8acefbf218f25bd5120cea6ad23c4d5aab Mon Sep 17 00:00:00 2001 From: WebCoder49 Date: Fri, 15 Dec 2023 09:11:28 +0000 Subject: [PATCH 03/11] Fix some CSS for line-numbers plugin --- code-input.css | 2 ++ code-input.js | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/code-input.css b/code-input.css index 0344708..df1c811 100644 --- a/code-input.css +++ b/code-input.css @@ -45,10 +45,12 @@ code-input textarea, code-input:not(.code-input_pre-element-styled) pre code, co border: 0; min-width: calc(100% - var(--padding, 16px) * 2); min-height: calc(100% - var(--padding, 16px) * 2); + height: max-content; overflow: hidden; resize: none; grid-row: 1; grid-column: 1; + height: max-content; } code-input:not(.code-input_pre-element-styled) pre, code-input.code-input_pre-element-styled pre code { diff --git a/code-input.js b/code-input.js index 770dc4a..bdc3bdf 100644 --- a/code-input.js +++ b/code-input.js @@ -533,7 +533,6 @@ var codeInput = { // Sync content if(this.needsHighlight) { - console.log("Update"); this.update(); this.needsHighlight = false; // this.needsDisableDuplicateSearching = true; From 9f835bac06f2038afdb5ec6aee0e5cb07a2ad6c6 Mon Sep 17 00:00:00 2001 From: WebCoder49 Date: Sun, 17 Dec 2023 09:44:59 +0000 Subject: [PATCH 04/11] Allow scrolling when highlight.js --- code-input.css | 4 +++- code-input.js | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/code-input.css b/code-input.css index df1c811..f7ee0f9 100644 --- a/code-input.css +++ b/code-input.css @@ -45,12 +45,14 @@ code-input textarea, code-input:not(.code-input_pre-element-styled) pre code, co border: 0; min-width: calc(100% - var(--padding, 16px) * 2); min-height: calc(100% - var(--padding, 16px) * 2); - height: max-content; overflow: hidden; resize: none; grid-row: 1; grid-column: 1; +} +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 { diff --git a/code-input.js b/code-input.js index bdc3bdf..946927a 100644 --- a/code-input.js +++ b/code-input.js @@ -528,8 +528,13 @@ var codeInput = { */ animateFrame() { // Sync size - this.textareaElement.style.height = getComputedStyle(this.preElement).height; - this.textareaElement.style.width = getComputedStyle(this.preElement).width; + if(this.template.preElementStyled) { + this.textareaElement.style.height = getComputedStyle(this.preElement).height; + this.textareaElement.style.width = getComputedStyle(this.preElement).width; + } else { + this.textareaElement.style.height = getComputedStyle(this.codeElement).height; + this.textareaElement.style.width = getComputedStyle(this.codeElement).width; + } // Sync content if(this.needsHighlight) { From c91d9da491022dae82bb827fdffb9965cde85586 Mon Sep 17 00:00:00 2001 From: WebCoder49 Date: Sat, 20 Jan 2024 18:02:23 +0000 Subject: [PATCH 05/11] Remove old fix for Ctrl+F due to its inconsistency --- CONTRIBUTING.md | 10 ++++++++- README.md | 5 ----- code-input.js | 55 +------------------------------------------------ 3 files changed, 10 insertions(+), 60 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52f8649..7a1be18 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: 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.js b/code-input.js index 946927a..a036e1e 100644 --- a/code-input.js +++ b/code-input.js @@ -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) => { @@ -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,7 +252,6 @@ var codeInput = { false, // preElementStyled true, // isCode false, // includeCodeInputInHighlightFunc - true, // autoDisableDuplicateSearching plugins ); }, @@ -297,7 +278,6 @@ var codeInput = { includeCodeInputInHighlightFunc: true, preElementStyled: true, isCode: false, - autoDisableDuplicateSearching: true, plugins: plugins, } }, @@ -322,7 +302,6 @@ var codeInput = { includeCodeInputInHighlightFunc: true, preElementStyled: true, isCode: false, - autoDisableDuplicateSearching: true, rainbowColors: rainbowColors, delimiter: delimiter, @@ -514,7 +493,6 @@ var codeInput = { * to syntax-highlight it. */ needsHighlight = false; // Just inputted - // needsDisableDuplicateSearching = false; // Just highlighted /** * Highlight the code ASAP @@ -540,14 +518,7 @@ var codeInput = { if(this.needsHighlight) { this.update(); this.needsHighlight = false; - // this.needsDisableDuplicateSearching = true; } - // if(this.needsDisableDuplicateSearching && this.codeElement.querySelector("*") != null) { - // // Has been highlighted - // this.resultElementDisableSearching(); - // this.needsDisableDuplicateSearching = false; - // } - window.requestAnimationFrame(this.animateFrame.bind(this)); } @@ -592,30 +563,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

From 1df873a3f047b83ed239a4880f859aa0aa234b71 Mon Sep 17 00:00:00 2001
From: WebCoder49 
Date: Sat, 10 Feb 2024 16:06:28 +0000
Subject: [PATCH 06/11] Add tests, make work for Prism; Clean up code

---
 code-input.css                 |  36 ++--
 code-input.js                  | 121 +++----------
 plugins/README.md              |   7 -
 plugins/auto-close-brackets.js |  24 +--
 plugins/autocomplete.js        |  17 +-
 plugins/autodetect.js          |   3 +-
 plugins/debounce-update.js     |  40 -----
 plugins/debounce-update.min.js |   1 -
 plugins/go-to-line.css         |  33 ++--
 plugins/go-to-line.js          |  77 +++++----
 plugins/indent.js              |   4 +-
 plugins/special-chars.css      |   4 -
 plugins/special-chars.js       |  98 +++++------
 plugins/test.js                |   3 +-
 tests/hljs.html                |  50 ++++++
 tests/prism.html               |  64 +++++++
 tests/tester.js                | 305 +++++++++++++++++++++++++++++++++
 17 files changed, 597 insertions(+), 290 deletions(-)
 delete mode 100644 plugins/debounce-update.js
 delete mode 100644 plugins/debounce-update.min.js
 create mode 100644 tests/hljs.html
 create mode 100644 tests/prism.html
 create mode 100644 tests/tester.js

diff --git a/code-input.css b/code-input.css
index f7ee0f9..4e119a8 100644
--- a/code-input.css
+++ b/code-input.css
@@ -4,19 +4,19 @@
 
 
 code-input {
-  /* Allow other elems to be inside */
-  position: relative;
-  top: 0;
-  left: 0;
+  /* Allow other elements to be inside */
   display: block;
   overflow-y: auto;
   overflow-x: auto;
 
+  position: relative;
+  top: 0;
+  left: 0;
+
   /* Normal inline styles */
   margin: 8px;
   --padding: 16px;
   height: 250px;
-
   font-size: normal;
   font-family: monospace;
   line-height: 1.5; /* Inherited to child elements */
@@ -24,10 +24,9 @@ code-input {
   caret-color: darkgrey;
   white-space: pre;
   padding: 0!important; /* Use --padding */
-
   display: grid;
-  grid-template-columns: auto;
-  grid-template-rows: auto;
+  grid-template-columns: 100%;
+  grid-template-rows: 100%;
 }
 
 
@@ -52,7 +51,7 @@ code-input textarea, code-input:not(.code-input_pre-element-styled) pre code, co
 }
 code-input:not(.code-input_pre-element-styled) pre code, code-input.code-input_pre-element-styled pre {
   height: max-content;
-  width: max-content;  
+  width: max-content;
 }
 
 code-input:not(.code-input_pre-element-styled) pre, code-input.code-input_pre-element-styled pre code {
@@ -71,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 {
@@ -92,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;
 }
 
@@ -121,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.js b/code-input.js
index a036e1e..c1138a0 100644
--- a/code-input.js
+++ b/code-input.js
@@ -29,7 +29,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",
@@ -124,7 +123,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}`);
@@ -138,7 +137,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
                 }
             }
@@ -257,9 +256,7 @@ var codeInput = {
         },
 
         /**
-         * 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 {
@@ -283,11 +280,7 @@ var codeInput = {
         },
 
         /**
-         * 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 {
@@ -311,13 +304,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);
@@ -379,14 +372,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);
             });
         }
 
@@ -495,7 +481,7 @@ var codeInput = {
         needsHighlight = false; // Just inputted
 
         /**
-         * Highlight the code ASAP
+         * Highlight the code as soon as possible
          */
         scheduleHighlight() {
             this.needsHighlight = true;
@@ -505,16 +491,18 @@ var codeInput = {
          * Call an animation frame
          */
         animateFrame() {
-            // Sync size
+            // 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;
             }
 
-            // Sync content
+            // Synchronise the contents of the pre/code and textarea elements
             if(this.needsHighlight) {
                 this.update();
                 this.needsHighlight = false;
@@ -600,7 +588,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
@@ -619,19 +607,12 @@ 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) => { this.value = this.textareaElement.value; });
 
@@ -672,7 +653,7 @@ var codeInput = {
         }
 
         /**
-         * @deprecated Please use `codeInput.CodeInput.escapeHtml`
+         * @deprecated Please use `codeInput.CodeInput.getTemplate`
          */
         get_template() {
             return this.getTemplate();
@@ -716,13 +697,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));
                 }
             }
         }
@@ -746,9 +722,6 @@ 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");
@@ -759,6 +732,7 @@ var codeInput = {
                         break;
 
                     case "lang":
+                    case "language":
 
                         let code = this.codeElement;
                         let mainTextarea = this.textareaElement;
@@ -790,7 +764,7 @@ var codeInput = {
 
                         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 {
@@ -968,30 +942,6 @@ var codeInput = {
         };
     },
 
-    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)
@@ -1009,29 +959,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..0d4f362 100644
--- a/plugins/auto-close-brackets.js
+++ b/plugins/auto-close-brackets.js
@@ -22,20 +22,24 @@ codeInput.plugins.AutoCloseBrackets = class extends codeInput.Plugin {
         let textarea = codeInput.textareaElement;
         textarea.addEventListener('keydown', (event) => { this.checkBackspace(codeInput, event) });
         textarea.addEventListener('beforeinput', (event) => { this.checkBrackets(codeInput, event); });
-
     }
 
     /* Event handlers */
     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
             let closingBracket = this.bracketPairs[event.data];
-            this.bracketsOpenedStack.push([closingBracket, codeInput.textareaElement.selectionStart]);
             document.execCommand("insertText", false, closingBracket);
             codeInput.textareaElement.selectionStart = codeInput.textareaElement.selectionEnd -= 1;
         }
@@ -43,11 +47,11 @@ codeInput.plugins.AutoCloseBrackets = class extends codeInput.Plugin {
 
     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..7f98828 100644
--- a/plugins/autocomplete.js
+++ b/plugins/autocomplete.js
@@ -29,12 +29,19 @@ codeInput.plugins.Autocomplete = class extends codeInput.Plugin {
         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");
+        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("input", 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
     }
diff --git a/plugins/autodetect.js b/plugins/autodetect.js
index 9ca5f01..3d0c07d 100644
--- a/plugins/autodetect.js
+++ b/plugins/autodetect.js
@@ -20,9 +20,10 @@ codeInput.plugins.Autodetect = class extends codeInput.Plugin {
         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 7d8e1b3..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`
-            codeInput.value = 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..d8a9e43 100644
--- a/plugins/go-to-line.js
+++ b/plugins/go-to-line.js
@@ -36,20 +36,25 @@ 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) return dialog.input.classList.add('code-input_go-to_error');
 
-        if(querySplitByColons.length >= 2) {
-            columnNo = Number(querySplitByColons[1]);
-            maxColumnNo = lines[lineNo-1].length;
-        }
 
         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');
+                }
             }
         }
 
@@ -65,15 +70,14 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin {
         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,29 +85,34 @@ 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('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);
+            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 */
diff --git a/plugins/indent.js b/plugins/indent.js
index 36aa418..cd4dfab 100644
--- a/plugins/indent.js
+++ b/plugins/indent.js
@@ -106,7 +106,7 @@ codeInput.plugins.Indent = class extends codeInput.Plugin {
         }
         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;
@@ -204,7 +204,7 @@ codeInput.plugins.Indent = class extends codeInput.Plugin {
 
     checkBackspace(codeInput, event) {
         if(event.key != "Backspace" || this.indentationNumChars == 1) {
-            return; // Normal backspace
+            return; // Normal backspace when indentation of 1
         }
 
         let inputElement = codeInput.textareaElement;
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 dc14256..6544417 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,11 +31,6 @@ 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) */
     afterElementsAdded(codeInput) {
         // For some reason, special chars aren't synced the first time - TODO is there a cleaner way to do this?
@@ -44,16 +39,15 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
 
     /* Runs after code is highlighted; Params: codeInput element) */
     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;
     }
 
     recursivelyReplaceText(codeInput, element) {
@@ -90,29 +84,29 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
         }
     }
 
-    specialCharReplacer(codeInput, match_char) {
-        let hex_code = match_char.codePointAt(0);
+    specialCharReplacer(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 +117,70 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
         return result;
     }
     
-    getCharacterColor(ascii_code) {
+    getCharacterColors(asciiCode) {
         // 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)) {
+        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) {
+    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..e383a0f
--- /dev/null
+++ b/tests/hljs.html
@@ -0,0 +1,50 @@
+
+
+
+    
+    
+    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
+ console.log("Hello, World!"); + + + + \ No newline at end of file diff --git a/tests/prism.html b/tests/prism.html new file mode 100644 index 0000000..380c30d --- /dev/null +++ b/tests/prism.html @@ -0,0 +1,64 @@ + + + + + + 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
+ console.log("Hello, World!"); + + + + \ No newline at end of file diff --git a/tests/tester.js b/tests/tester.js new file mode 100644 index 0000000..b0ed56c --- /dev/null +++ b/tests/tester.js @@ -0,0 +1,305 @@ +/* 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); + assertEqual(group, "Text Output", textarea.value, origValueBefore+correctOutput+origValueAfter) + 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 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(); + + /*--- 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"); + window.setTimeout(() => { + testAssertion("Autocomplete", "Popup Shows", confirm("Does the autocomplete popup display correctly? (OK=Yes)"), "user-judged"); + backspace(textarea); + window.setTimeout(() => { + testAssertion("Autocomplete", "Popup Disappears", confirm("Has the popup disappeared? (OK=Yes)"), "user-judged"); + backspace(textarea); + backspace(textarea); + backspace(textarea); + backspace(textarea); + + // Autodetect + if(isHLJS) { + // Replace all code + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + backspace(textarea); + addText(textarea, "\n\n\n

Hello, World!

\n\n"); + assertEqual("Autodetect", "Detects HTML", textarea.parentElement.getAttribute("language"), "html"); + + + // Replace all code + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + backspace(textarea); + addText(textarea, "for i in range(100):\n print(i)"); + assertEqual("Autodetect", "Detects Python", textarea.parentElement.getAttribute("language"), "python"); + + // Replace all code + 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}"); + assertEqual("Autodetect", "Detects CSS", textarea.parentElement.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 = textarea.parentElement.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 = textarea.parentElement.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 = textarea.parentElement.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 = textarea.parentElement.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 = textarea.parentElement.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; + + window.setTimeout(() => { + 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); + }); + }, 50); + }, 100); + }, 100); +} \ No newline at end of file From 65a32fa1651542a5fbaca2f410f8a0b8bf0db2f6 Mon Sep 17 00:00:00 2001 From: WebCoder49 Date: Sat, 10 Feb 2024 17:20:33 +0000 Subject: [PATCH 07/11] Finish tests; fix code so they should work with Prism --- code-input.js | 15 +- tests/hljs.html | 9 +- tests/prism.html | 8 +- tests/tester.js | 384 ++++++++++++++++++++++++++++------------------- 4 files changed, 247 insertions(+), 169 deletions(-) diff --git a/code-input.js b/code-input.js index c1138a0..7142d4e 100644 --- a/code-input.js +++ b/code-input.js @@ -19,6 +19,7 @@ var codeInput = { observedAttributes: [ "value", "placeholder", + "language", "lang", "template" ], @@ -417,10 +418,6 @@ var codeInput = { constructor() { super(); // Element } - /** - * Store value internally - */ - _value = ''; /** * Exposed child textarea element for user to input code in @@ -633,7 +630,7 @@ var codeInput = { if (this.template.isCode) { if (lang != undefined && lang != "") { - code.classList.add("language-" + lang); + code.classList.add("language-" + lang.toLowerCase()); } } @@ -733,7 +730,6 @@ var codeInput = { case "lang": case "language": - let code = this.codeElement; let mainTextarea = this.textareaElement; @@ -849,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. @@ -859,7 +856,9 @@ var codeInput = { if (val === null || val === undefined) { val = ""; } - this._value = val; + // Save in editable textarea element + this.textareaElement.value = val; + // Trigger highlight this.needsHighlight = true; return val; } diff --git a/tests/hljs.html b/tests/hljs.html index e383a0f..6c500e1 100644 --- a/tests/hljs.html +++ b/tests/hljs.html @@ -35,13 +35,18 @@ + Make tester async function using await?-->

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
- console.log("Hello, World!"); +
+ console.log("Hello, World!"); +// A second line +// A third line with <html> tags + +
- + Make tester async function using await? + Stop enter in go-to dialog from submitting form-->

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
+
Test Results (Click to Open)
console.log("Hello, World!"); // A second line diff --git a/tests/prism.html b/tests/prism.html index bee822e..1aa59fb 100644 --- a/tests/prism.html +++ b/tests/prism.html @@ -31,7 +31,7 @@

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
+
Test Results (Click to Open)
console.log("Hello, World!"); // A second line @@ -39,29 +39,21 @@

Test for highlight.js

From 672a45cc5471e444265a8bc14408eaa267322eb5 Mon Sep 17 00:00:00 2001 From: WebCoder49 Date: Sun, 11 Feb 2024 18:05:10 +0100 Subject: [PATCH 09/11] Get all tests working for HLJS+Prism except for HLJS autocomplete --- plugins/go-to-line.js | 1 - tests/hljs.html | 2 +- tests/tester.js | 419 ++++++++++++++++++++++-------------------- 3 files changed, 222 insertions(+), 200 deletions(-) diff --git a/plugins/go-to-line.js b/plugins/go-to-line.js index 083879d..f6e0373 100644 --- a/plugins/go-to-line.js +++ b/plugins/go-to-line.js @@ -98,7 +98,6 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin { 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); }); diff --git a/tests/hljs.html b/tests/hljs.html index 4787487..3b13b11 100644 --- a/tests/hljs.html +++ b/tests/hljs.html @@ -8,7 +8,7 @@ - + diff --git a/tests/tester.js b/tests/tester.js index 94375b4..e0ea13f 100644 --- a/tests/tester.js +++ b/tests/tester.js @@ -129,7 +129,15 @@ function move(textarea, numMovesRight) { textarea.selectionEnd = textarea.selectionStart; } -function startTests(textarea, isHLJS) { +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){ @@ -157,219 +165,234 @@ function startTests(textarea, isHLJS) { // 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.");`; - window.setTimeout(() => { // 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!"); + + 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!"); + // 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 + + // 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"); - window.setTimeout(() => { // Wait for UI to update - 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"); - window.setTimeout(() => { // Wait for UI to update - 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(); - window.setTimeout(() => { // Wait for rendered value to update - assertEqual("Core", "Form Reset resets Code-Input Value", codeInputElement.value, `console.log("Hello, World!"); + + 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!"); + 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!"); + 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"); - window.setTimeout(() => { // Wait for popup to be rendered - testAssertion("Autocomplete", "Popup Shows", confirm("Does the autocomplete popup display correctly? (OK=Yes)"), "user-judged"); - backspace(textarea); - window.setTimeout(() => { // 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 - if(isHLJS) { - // Replace all code - textarea.selectionStart = 0; - textarea.selectionEnd = textarea.value.length; - backspace(textarea); - addText(textarea, "\n\n\n

Hello, World!

\n\n"); - assertEqual("Autodetect", "Detects HTML", codeInputElement.getAttribute("language"), "html"); - - - // Replace all code - textarea.selectionStart = 0; - textarea.selectionEnd = textarea.value.length; - backspace(textarea); - addText(textarea, "for i in range(100):\n print(i)"); - assertEqual("Autodetect", "Detects Python", codeInputElement.getAttribute("language"), "python"); - - // Replace all code - 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}"); - 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; - - window.setTimeout(() => { // 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); - }); - }, 50); - }, 100); - }, 100); - }, 50); - }, 50); - }, 50); - }, 100); + + /*--- 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 From fd08758433c1ea53dff87e0b0b9e2b5299b9cee3 Mon Sep 17 00:00:00 2001 From: WebCoder49 Date: Mon, 12 Feb 2024 18:58:15 +0100 Subject: [PATCH 10/11] Finish tests; make code work with all tests --- code-input.css | 6 +++--- plugins/autocomplete.js | 5 ++--- plugins/go-to-line.js | 9 +++++++-- tests/hljs.html | 7 ------- tests/prism.html | 15 --------------- 5 files changed, 12 insertions(+), 30 deletions(-) diff --git a/code-input.css b/code-input.css index 4e119a8..97a85bf 100644 --- a/code-input.css +++ b/code-input.css @@ -8,7 +8,6 @@ code-input { display: block; overflow-y: auto; overflow-x: auto; - position: relative; top: 0; left: 0; @@ -17,13 +16,13 @@ code-input { 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%; @@ -48,6 +47,7 @@ code-input textarea, code-input:not(.code-input_pre-element-styled) pre code, co 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; diff --git a/plugins/autocomplete.js b/plugins/autocomplete.js index c6e2cb7..24d5089 100644 --- a/plugins/autocomplete.js +++ b/plugins/autocomplete.js @@ -42,9 +42,8 @@ codeInput.plugins.Autocomplete = class extends codeInput.Plugin { } let textarea = codeInput.textareaElement; - textarea.addEventListener("input", 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 diff --git a/plugins/go-to-line.js b/plugins/go-to-line.js index f6e0373..eba0934 100644 --- a/plugins/go-to-line.js +++ b/plugins/go-to-line.js @@ -33,7 +33,6 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin { const querySplitByColons = dialog.input.value.split(':'); 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) { @@ -62,6 +61,7 @@ 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(); @@ -98,7 +98,12 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin { dialog.textarea = textarea; dialog.input = input; - input.addEventListener('keyup', (event) => { this.checkPrompt(dialog, event); }); + 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); diff --git a/tests/hljs.html b/tests/hljs.html index 3b13b11..a832801 100644 --- a/tests/hljs.html +++ b/tests/hljs.html @@ -32,13 +32,6 @@ -

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.

diff --git a/tests/prism.html b/tests/prism.html index 1aa59fb..71c2628 100644 --- a/tests/prism.html +++ b/tests/prism.html @@ -39,21 +39,6 @@

Test for highlight.js

From 79ce263b3be39c4c5e49e1d2cb37283954ce9b67 Mon Sep 17 00:00:00 2001 From: WebCoder49 Date: Mon, 12 Feb 2024 19:02:21 +0100 Subject: [PATCH 11/11] Clarify contributing guidelines for tests --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a1be18..ce82c71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,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.